Expiration and Timeouts
Separate server-side deadlines from client waits and choose a safe expiration policy.
There are three different clocks in an Action workflow:
| Clock | Controlled by | What it means |
|---|---|---|
expires_at | ActionBox | The server-side deadline after which an open Action becomes terminal. |
wait_seconds or an SDK/CLI timeout | Your client | How long one read or local wait is allowed to block. It never chooses a decision. |
| Callback attempt timeout | Callback delivery and your receiver | How long a callback attempt may run before it is retried or marked failed. |
Set a server-side deadline
Use a future RFC 3339 timestamp, preferably in UTC with an explicit Z:
{
"title": "Approve the maintenance window",
"expires_at": "2026-09-01T22:00:00Z",
"options": [
{"id": "ship", "label": "Ship it"},
{"id": "hold", "label": "Hold"}
]
}expires_at may be set when creating or updating an open Action. If it is
omitted, the Action has no server expiration. When ActionBox processes a due
Action, it transitions to expired; terminal state is immutable and cannot be
reopened.
By default, expiration returns an explicit expired result with no invented
answer. If the workflow has a known safe fallback, declare it with
on_expire.resolve. The fallback must include an interaction-compatible typed
response:
{
"expires_at": "2026-09-01T22:00:00Z",
"on_expire": {
"type": "resolve",
"response": {"type": "single_choice", "value": "hold"},
"reason": "The maintenance window closed without approval."
}
}An expiration fallback follows the same atomic resolution path as a human or
machine decision. It produces a resolved Action and a signed decision receipt
with resolved_by: "system". A normal expiration produces no decision receipt.
Never guess a default answer
Use on_expire.resolve only when the fallback is explicitly safe for that
workflow. Otherwise branch on expired and stop the operation or send it
through a new review.
Wait without changing state
Machine clients can long-poll the Source-scoped read endpoint for up to 30 seconds:
curl -fsS "https://api.actionbox.cloud/v1/source/actions/$ACTION_ID?wait_seconds=30" \
-H "Authorization: Bearer $ACTIONBOX_SOURCE_KEY"The response is the current Action. If it is still open, the server wait
ended without a terminal change; issue another bounded read when appropriate.
The SDKs use this route and cap each server wait at 30 seconds, so a worker does
not need a tight polling loop.
A local timeout has the same meaning as a read that returned open:
- Python
Action.wait(...)returnsNonewhile the Action remains open; - TypeScript
client.wait(...)returns the current Action, which may still be open; - the CLI's local wait timeout does not resolve or cancel the server Action.
After a process restart, re-read by Action ID and branch on the authoritative state. Do not turn a socket timeout into rejection.
State transitions
open ── human or machine response ──> resolved
open ── owning Source cancellation ─> cancelled
open ── deadline, no fallback ──────> expired
open ── deadline, safe fallback ────> resolved (system)Only open Actions transition. A competing decision, cancellation, or
expiration receives a conflict rather than overwriting the first terminal
event. See Errors and safe retries and Decisions and
outcomes.
Next: Creating Actions, Idempotency and dedupe, or Webhooks and callbacks.