Push is the primitive
A push targets a single device.start and end declare temporal intent:
| Shape | Body | Behaviour |
|---|---|---|
| Immediate | No start | Fire on receipt. |
| Scheduled | start only | Persist; fire at start. |
| Windowed | start and end | Run from start, revert at end. |
execution array stating which of these shapes it accepts.
start accepts plant-local wall-clock ISO 8601 (YYYY-MM-DDTHH:MM:SS, no Z, no offset) or a relative duration (30m, 1.5h, 2h). The platform interprets the wall-clock in the device’s plant timezone. start must be in the future. The horizon is 30 days; further-out submissions return 422 START_OUT_OF_RANGE.
Responses normalise start and end to absolute UTC. A scheduled or windowed push submitted as 2026-06-10T22:00:00 for a UK plant during BST reads back as 2026-06-10T21:00:00.000Z. You write wall-clock, you read the dispatch-ready instant. See canonical actions for the full contract.
Lifecycle states
Every action moves through a small state machine:| State | Meaning |
|---|---|
scheduled | Persisted, queued to fire at start. |
acknowledged | The platform has dispatched the command to the OEM. Initial state for an immediate push. |
completed | The OEM accepted the command. |
failed | The OEM rejected the command, or a transient failure exhausted retries. |
cancelled | POST /actions/{id}/cancel was called before dispatch. |
completed records that the OEM accepted the command, not that the device verified end-state. To confirm physical state, subscribe to push.completed and read the device on the webhook. The API does not retry a failed push; you decide whether to re-submit, and with what.
Subscribe to push.completed and push.failed via webhooks, or poll the action endpoint. Both are first-class.
Cancellation
POST /actions/{id}/cancel transitions a scheduled action to cancelled and removes it from the queue. Already-executing actions cannot be cancelled: a 409 ACTION_NOT_CANCELLABLE comes back. Amps does not attempt to recall in-flight OEM calls.
Dispatch guarantees
Every scheduled action dispatches to the OEM exactly once. If the platform retries delivery, the OEM is never called twice for the same action.Schedules as device config
Coming soon. Setting a schedule is published ahead of its implementation: every schedule write returns
501 NOT_IMPLEMENTED until the scheduler ships. Two things work today: action-level scheduling via start and end on a push, and the follow_schedule command, which tells a device to run whatever schedule it currently holds. The shape below is the contract you will write to.PUT /{type}/{id}/schedule. There is no separate resource to create a schedule on, because a schedule belongs to one device. The body is { "slots": [...] }, where each slot carries a command and its temporal intent. GET /{type}/{id}/schedule reads it back; DELETE /{type}/{id}/schedule clears it.
A slot is a push action plus an optional recurrence. The slot shape is derived from the action shape, so the fields you already use on a push are the fields you use in a slot: a slot with no recurrence is exactly a one-off deferred action expressed in schedule form. Whether GET /{type}/{id}/schedule returns in-flight deferred actions as slots is a decision left for when the scheduler ships.
A one-off slot
A slot withoutrecurrence fires once. It is the slot form of a deferred or windowed push.
A recurring slot
Addrecurrence to repeat the same command across days, weeks, or months. The slot’s start and end define the wall-clock window the recurrence steps through.
Recurrence fields
recurrence is iCal-shaped: a small set of fields, each optional except frequency.
| Field | Type | Meaning |
|---|---|---|
frequency | daily | weekly | monthly | How often the slot repeats. Required. |
interval | positive integer | Repeat every N periods. Default 1. interval: 2 with frequency: weekly is fortnightly. |
byDay | array of weekday names | Weekly only: the days the slot fires (e.g. ["saturday", "sunday"]). |
byMonthDay | integer 1-31 | Monthly only: the day of the month the slot fires. |
until | wall-clock ISO 8601 | Plant-local wall-clock after which the slot stops repeating. Omit for open-ended. |
slots array is the authoritative record of what you asked for. The platform expands the recurrence to drive dispatch; the slot definitions are what you wrote.
The write is a full replace, never a partial edit. PUT /{type}/{id}/schedule overwrites the previous schedule wholesale, so the device holds exactly what you last sent. DELETE /{type}/{id}/schedule clears it.
Listing schedules across devices
To see every schedule at once rather than device by device, the cross-device read will beGET /schedules, the same way GET /actions is the cross-device read of actions across your fleet. GET /schedules/{id} will read a single schedule by its handle for the list-to-detail hop. Both routes return 501 NOT_IMPLEMENTED today and arrive with the rest of the scheduler. The list will be the only lifted read; setting, changing, and clearing a schedule stays on the device-scoped routes above.
Following a schedule today
Thefollow_schedule command works now. Send it on a push (POST /{type}/{id} with { "action": { "command": "follow_schedule" } }) and the device runs whatever schedule it currently holds. It conflicts with the operating-mode commands like charge, so the one-action-per-device invariant holds: a device either follows its schedule or runs a direct command, not both at once. Setting the schedule’s contents is what comes later; telling a device to follow one is available today.
One contract across OEMs
The same schedule surface and response shape apply across every supported OEM, regardless of how the device runs the schedule under the hood. The device’sscheduling capability block declares its limits; no request field selects how execution happens. You write the schedule once, the platform delivers it.
Conflict and overlap
A new action that overlaps an existing pending or active action on the same device returns 409CONFLICT with conflictingActionIds[], unless onConflict resolves it. Amps does not auto-cancel existing schedules on a conflicting submission. Read conflict resolution for the full model, including how the platform handles native device schedulers that are already running.
Amps does not poll devices to detect drift between scheduled intent and physical state. Schedules are declarative on submission; the platform delivers them and the device runs them.
What scheduling does not do
- No price or tariff inputs. Slots carry wall-clock times. Tariff optimisation stays on your side, and the control plane stays decoupled from market data feeds.
- No pause and resume. The
pausedstate is reserved in the enum but unused. Cancel and re-create instead. - No priorities or preemption. Conflicts surface as 409, and you drive resolution.
- No fleet or multi-device actions. Every action targets one
deviceId. Aggregation across devices is the caller’s responsibility.
Frequently asked questions
What states can a scheduled action be in?
Five:scheduled (persisted, queued to fire at start), acknowledged (the platform is talking to the OEM), completed (the OEM accepted), failed (the OEM rejected, or transient retries exhausted), and cancelled (a client cancelled before dispatch). scheduled and acknowledged are non-terminal. The other three are terminal. Actions never move backward through the state machine.
Can I cancel a scheduled action?
Yes, while it is inscheduled. POST /actions/{id}/cancel transitions the action to cancelled and removes it from the queue. Already-executing actions (acknowledged) cannot be cancelled and return 409 ACTION_NOT_CANCELLABLE. Amps does not attempt to recall in-flight OEM calls. When the scheduler ships, DELETE /{type}/{id}/schedule will clear a device’s schedule and cancel every pending child action it spawned.
What happens if my schedule conflicts with another scheduled action?
The new action returns 409CONFLICT with conflictingActionIds[] if onConflict is absent. Supply cancel_and_replace to drop the conflicting action and run the new one, or queue_after to defer the new action until the conflicting one’s end. Window-overlap predicates are not part of the test: two scheduled discharges for the same device collide on submission even with disjoint times. The model trades flexibility for an unambiguous one-action-per-device invariant.
How does recurrence work?
Recurrence ships when the scheduler lands. A slot with arecurrence field repeats on frequency (daily, weekly, or monthly), stepped by an optional interval. Weekly recurrences pick specific weekdays via byDay; monthly recurrences pick a day-of-month via byMonthDay. An until wall-clock bounds the recurrence; omit it for open-ended. Each repetition becomes a child action that moves through the same lifecycle as a direct push. Clearing the device’s schedule cancels every pending child action.
Is a one-off slot the same as a deferred push?
In intent, yes. A slot with norecurrence carries the same fields as a deferred push (command, start, optional end, parameters) and produces the same one-shot dispatch. The two surfaces will converge in practice; whether GET /{type}/{id}/schedule surfaces in-flight deferred actions as slots is decided when the scheduler ships.
Related concepts
Canonical Actions
The shape every push uses, and the shape every slot extends.
Conflict Resolution
onConflict, 409 envelopes, foreign-state preflight.Webhooks
Action and schedule lifecycle events delivered to your endpoint.
Error Envelope
START_IN_PAST, START_OUT_OF_RANGE, EXECUTION_NOT_SUPPORTED.