A device may already have a non-terminal action attached when your push lands. TheDocumentation Index
Fetch the complete documentation index at: https://docs.amps.ai/llms.txt
Use this file to discover all available pages before exploring further.
onConflict field decides what happens when the new push collides with the existing one. Two strategies are accepted. Omit the field to get a 409 you can act on.
The two strategies
| Strategy | Behaviour |
|---|---|
cancel_and_replace | Cancel the conflicting action. Run the new one. The replaced action transitions to cancelled. |
queue_after | Defer the new action’s start to the conflict’s end. The new action persists in scheduled. |
queue_after requires the conflicting action to declare an end. A push with queue_after against an open-ended conflict is rejected.
What counts as a conflict
At most one non-terminal action per(device, action-type) pair. A non-terminal state is any of scheduled or acknowledged. Terminal states (completed, failed, cancelled) do not block new pushes.
A second push for the same device and the same action type, while a non-terminal one exists, is a conflict. Overlap in time is not part of the test: two scheduled discharges for the same device, even with disjoint time windows, will collide on submission.
Window-overlap predicates that allow disjoint schedules to coexist are forthcoming. The current model maintains a single-active-action invariant per device.
Without onConflict: 409
Submit a push withoutonConflict while a conflict exists, and the API returns:
conflictingActionIds is always an array, even for a single conflict.
Foreign state: SCHEDULER_ACTIVE
A second class of conflict lives on the OEM side. Many batteries have a native scheduler that can hold rows from the homeowner’s mobile app, a third-party optimiser, or a previous Amps action that is no longer tracked. Before writing a windowed-mode push to such a device, Amps runs a foreign-state preflight.reason | Trigger |
|---|---|
foreign_scheduler_window_overlap | Foreign scheduler has rows that overlap the candidate window. |
foreign_scheduler_blocking_imperative | Foreign scheduler is actively running and the OEM blocks an imperative write while it runs. |
foreign_scheduler_groups_present | The platform refused a whole-replace write because foreign rows exist. |
SCHEDULER_ACTIVE is the canonical name for any device-side conflict, distinct from CONFLICT, which names Amps-tracked conflicts. Both are 409. The recoveryStrategies array tells you what to try next: cancel_and_replace invokes the slot-aware merge (drop only overlapping rows, preserve the rest) on the OEM, while manual_clear_via_device_app is the escape hatch when the foreign owner is the homeowner and Amps cannot safely overwrite their schedule.
Slot-aware merge under cancel_and_replace
cancel_and_replace drops only overlapping rows on the device-side scheduler. Non-overlapping foreign rows are preserved. If the merged set would exceed the device’s slot limit, the API refuses with SCHEDULER_FULL carrying details.existingGroupCount and details.maxGroupCount.
Cleanup ownership
Recurring slots carry an ownership marker so Amps can clean them up at end-of-window without disturbing rows authored by other systems. Cleanup is idempotent and retries on transient failures.Detection timing
Foreign-state checks run twice on OEMs that expose a foreign-state read:- Submission-time preflight. When you submit a windowed-mode push without
cancel_and_replace, the API checks the OEM’s current scheduler state and refuses with409 SCHEDULER_ACTIVEif there is overlap.details.detectedAt: 'submission_time'. - Dispatch-time merge. When the scheduled fire moment arrives, the platform reads the OEM’s current state again and computes overlap. Foreign state can change between submission and dispatch, so the merge runs the same logic with
details.detectedAt: 'dispatch_time'.
Time and timezone constraints
Some OEM schedulers interpret time fields in plant-local wall-clock terms, so windowed-mode pushes against those OEMs reject ambiguous shapes. ISO withZ or +00:00/-00:00 is rejected because it would silently shift a UK BST schedule by an hour. Use an unambiguous offset, like +01:00. Relative durations (1h, 30m) are rejected on end for the same reason. The rejection is INVALID_TIME_WINDOW with a details.reason naming the specific issue. The constraint lifts once device.timeZone is available server-side.
Frequently asked questions
When does the API return 409 CONFLICT?
When a new push lands on a device that already has a non-terminal action (scheduled or acknowledged) and the request body omits onConflict. The 409 carries conflictingActionIds[] and the strategies the platform supports for resolution. Submit again with onConflict set to one of those strategies and the platform applies it. Terminal actions (completed, failed, cancelled) do not block new pushes.
What is the difference between cancel_and_replace and queue_after?
cancel_and_replace cancels the conflicting action and runs the new one immediately. The replaced action transitions to cancelled. queue_after defers the new action’s start to the conflict’s end and persists it in scheduled. queue_after requires the conflicting action to declare an end; against an open-ended conflict (e.g., an auto.balanced with no end) the API rejects.
How do OEM-side scheduler conflicts surface?
As 409SCHEDULER_ACTIVE with details.reason naming one of three triggers: foreign_scheduler_window_overlap (foreign rows overlap the candidate window), foreign_scheduler_blocking_imperative (the OEM blocks an imperative write while a foreign program runs), or foreign_scheduler_groups_present (the platform refused a whole-replace write because foreign rows exist). The response carries recoveryStrategies (typically cancel_and_replace, manual_clear_via_device_app).
Does cancel_and_replace wipe the entire OEM scheduler?
No.cancel_and_replace drops only overlapping rows. Non-overlapping foreign rows are preserved. Rows authored by Amps carry an ownership marker; cleanup removes only those.
What if my windowed push hits the device’s slot limit?
The API returnsSCHEDULER_FULL with details.existingGroupCount and details.maxGroupCount. The exact limit varies by device. Amps does not auto-prune to make space. The caller decides: cancel an existing schedule, ask the homeowner to clear rows from the OEM app, or accept the rejection.
Related concepts
Canonical Actions
The shape that carries
onConflict.Scheduling
Where conflicts arise: lifecycle, slots, recurrence.
Error Envelope
CONFLICT, SCHEDULER_ACTIVE, SCHEDULER_FULL, INVALID_TIME_WINDOW.Capabilities
Which strategies a device exposes.