A 409 means the device already has an action booked for the requested window. The error response lists the colliding action IDs and the strategies that would resolve it. Two strategies, cancel_and_replace and queue_after, ride on the request body so you can resolve conflicts in a single round-trip.A different kind of conflict comes from the device’s own scheduler. When an OEM has its native scheduler active, direct writes are rejected until it is cleared. You see this as SCHEDULER_ACTIVE on the action result.
Use this when the new intent should run as soon as the device is free, without losing the existing schedule. The new action is deferred until the active one terminates.
cancel_and_replace only works while the conflicting action is scheduled. If the OEM has already accepted the write, you get a 409 with the in-flight IDs and no strategies that would resolve it.
{ "success": false, "error": { "code": "CONFLICT", "message": "The conflicting action is already in progress and cannot be cancelled. Wait for it to complete or fail.", "details": { "conflictingActionIds": ["act_inflight_002"] } }, "meta": { "requestId": "req_3dC6hOsT", "timestamp": "2026-05-08T12:00:03.000Z", "path": "/battery/dev_abc123", "latencyMs": 18 }}
Poll the in-flight action. Retry the push once it reaches completed, failed, or cancelled.
Some OEMs, FoxESS-class devices among them, require their native scheduler to be disabled before they will accept direct mode writes. When the scheduler is active, the action result carries SCHEDULER_ACTIVE.
{ "id": "act_failed_003", "deviceId": "dev_abc123", "type": "battery:set_operation_mode", "state": "failed", "parameters": { "mode": "charge", "target": { "value": 95, "unit": "percent" } }, "result": null, "errorCode": "SCHEDULER_ACTIVE", "errorMessage": "Native scheduler is active; clear it before issuing a direct command.", "createdAt": "2026-05-08T12:10:00.000Z", "updatedAt": "2026-05-08T12:10:32.000Z", "acknowledgedAt": "2026-05-08T12:10:01.000Z", "completedAt": "2026-05-08T12:10:32.000Z"}
Resolve by re-pushing the same action with onConflict: "cancel_and_replace". The native scheduler is cleared, the direct command is applied, and the new action’s lifecycle reports back as normal. No separate cancel call is required.