Skip to main content
Scheduling on Amps has one execution primitive: a push. A push fires immediately, at a future instant, or across a window. A schedule sits on the device as configuration, made of slots, where each slot is a push action plus an optional recurrence. Understand the push and you understand scheduling.

Push is the primitive

A push targets a single device. start and end declare temporal intent:
ShapeBodyBehaviour
ImmediateNo startFire on receipt.
Scheduledstart onlyPersist; fire at start.
Windowedstart and endRun from start, revert at end.
The same canonical body works for all three, defined on canonical actions. Each command on a device declares an execution array stating which of these shapes it accepts.
{
  "action": {
    "command": "charge",
    "parameters": { "power": { "value": 5, "unit": "kw" } },
    "start": "2026-06-01T22:00:00",
    "end":   "2026-06-02T05:00:00"
  }
}
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:
StateMeaning
scheduledPersisted, queued to fire at start.
acknowledgedThe platform has dispatched the command to the OEM. Initial state for an immediate push.
completedThe OEM accepted the command.
failedThe OEM rejected the command, or a transient failure exhausted retries.
cancelledPOST /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.
A schedule is device configuration. You set it on the device, the same way you set settings: 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 without recurrence fires once. It is the slot form of a deferred or windowed push.
PUT /battery/device_abc123/schedule

{
  "slots": [
    {
      "command": "charge",
      "start": "2026-06-01T22:00:00",
      "end":   "2026-06-02T05:00:00",
      "parameters": { "target": { "value": 80, "unit": "percent" } }
    }
  ]
}

A recurring slot

Add recurrence 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.
PUT /battery/device_abc123/schedule

{
  "slots": [
    {
      "command": "charge",
      "start": "2026-06-01T22:00:00",
      "end":   "2026-06-02T05:00:00",
      "parameters": { "target": { "value": 80, "unit": "percent" } },
      "recurrence": { "frequency": "daily" }
    }
  ]
}

Recurrence fields

recurrence is iCal-shaped: a small set of fields, each optional except frequency.
FieldTypeMeaning
frequencydaily | weekly | monthlyHow often the slot repeats. Required.
intervalpositive integerRepeat every N periods. Default 1. interval: 2 with frequency: weekly is fortnightly.
byDayarray of weekday namesWeekly only: the days the slot fires (e.g. ["saturday", "sunday"]).
byMonthDayinteger 1-31Monthly only: the day of the month the slot fires.
untilwall-clock ISO 8601Plant-local wall-clock after which the slot stops repeating. Omit for open-ended.
A weekly slot that only fires on weekends:
{
  "command": "discharge",
  "start": "2026-06-06T16:00:00",
  "end":   "2026-06-06T19:00:00",
  "parameters": { "target": { "value": 30, "unit": "percent" } },
  "recurrence": {
    "frequency": "weekly",
    "byDay": ["saturday", "sunday"],
    "until": "2026-09-30T00:00:00"
  }
}
The 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 be GET /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

The follow_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’s scheduling 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 409 CONFLICT 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 paused state 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 in scheduled. 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 409 CONFLICT 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 a recurrence 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 no recurrence 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.

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.
For worked examples, see the cookbook: schedule a charge for later and discharge during a peak window.