Triggers and Subscriptions API
Triggers are named event sources (one-shot delayed, recurring cron, or webhook) that fire subscriptions, which in turn post chat messages, start fresh agent or graph sessions, or resume parked sessions. A running agent can also park itself on a trigger via subscribe_to_trigger and resume automatically when the trigger fires.
The park/resume model, subscription kinds, and webhook setup.
Create and manage triggers in the console.
Endpoints
| Method | Path | Summary |
|---|---|---|
| GET | /v1/triggers |
List triggers |
| POST | /v1/triggers |
Create a trigger |
| GET | /v1/triggers/{trigger_id} |
Get a trigger |
| PUT | /v1/triggers/{trigger_id} |
Update a trigger (partial) |
| DELETE | /v1/triggers/{trigger_id} |
Delete a trigger (cascades to subscriptions) |
| POST | /v1/triggers/{trigger_id}/fire_now |
Synchronously fire a trigger |
| POST | /v1/triggers/{trigger_id}/rotate_token |
Rotate a webhook trigger's URL token |
| GET | /v1/triggers/{trigger_id}/subscriptions |
List subscriptions for a trigger |
| POST | /v1/triggers/{trigger_id}/subscriptions |
Create a subscription |
| GET | /v1/triggers/{trigger_id}/subscriptions/{subscription_id} |
Get a subscription |
| PUT | /v1/triggers/{trigger_id}/subscriptions/{subscription_id} |
Replace a subscription |
| DELETE | /v1/triggers/{trigger_id}/subscriptions/{subscription_id} |
Delete a subscription |
Trigger object
{
"id": "daily-report",
"slug": "daily-report",
"name": "Daily Report",
"description": "Fires every morning at 08:00 UTC.",
"config": {
"kind": "scheduled",
"cron": "0 8 * * *",
"timezone": "UTC",
"catchup": "one"
},
"enabled": true
}
| Field | Required | Type | Description |
|---|---|---|---|
slug |
yes | string | URL-safe identifier (2-64 chars) |
name |
yes | string | Human-readable label (1-200 chars) |
description |
no | string | Optional description (max 2000 chars) |
config |
yes | TriggerConfig | A delayed, scheduled, or webhook config (see below) |
enabled |
no | boolean | Default true; when false the trigger never fires |
Delayed trigger config (kind: "delayed"):
| Field | Required | Description |
|---|---|---|
kind |
yes | Must be "delayed" |
fire_at |
yes | ISO 8601 datetime (UTC); the trigger fires once at this time |
Scheduled trigger config (kind: "scheduled"):
| Field | Required | Description |
|---|---|---|
kind |
yes | Must be "scheduled" |
cron |
yes | Standard 5-field cron expression |
timezone |
no | IANA timezone name; default "UTC" |
catchup |
no | "one" (default), "all", or "none": controls missed-fire catch-up behavior |
Webhook trigger config (kind: "webhook"):
| Field | Required | Description |
|---|---|---|
kind |
yes | Must be "webhook" |
token |
no | Server-minted URL token (32 hex chars) embedded in POST /v1/webhooks/{token}. On create it is always minted server-side (any supplied value is ignored); rotate it with POST /v1/triggers/{trigger_id}/rotate_token |
hmac_secret |
no | When set, callers must send X-Primer-Signature: HMAC-SHA256(secret, raw_body) as a lowercase hex digest; requests that fail verification are rejected 401. Write-only |
Create a trigger
POST /v1/triggers - returns 201 Created with the trigger object.
curl -X POST https://your-host/v1/triggers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"slug": "daily-report",
"name": "Daily Report",
"config": {
"kind": "scheduled",
"cron": "0 8 * * *",
"timezone": "UTC",
"catchup": "one"
},
"enabled": true
}'import httpx
r = httpx.post(
"https://your-host/v1/triggers",
headers={"Authorization": f"Bearer {token}"},
json={
"slug": "daily-report",
"name": "Daily Report",
"config": {
"kind": "scheduled",
"cron": "0 8 * * *",
"timezone": "UTC",
"catchup": "one",
},
"enabled": True,
},
)
assert r.status_code == 201const r = await fetch("/v1/triggers", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
body: JSON.stringify({
slug: "daily-report",
name: "Daily Report",
config: {kind: "scheduled", cron: "0 8 * * *", timezone: "UTC", catchup: "one"},
enabled: true
})
})Errors:
422- validation failed (missing required field, invalid cron, badfire_atformat)409- a trigger with thisslugalready exists
Get a trigger
GET /v1/triggers/{trigger_id} - returns 200 OK with the trigger object.
curl https://your-host/v1/triggers/daily-report \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get("https://your-host/v1/triggers/daily-report",
headers={"Authorization": f"Bearer {token}"})const r = await fetch("/v1/triggers/daily-report", {
headers: {"Authorization": `Bearer ${token}`}
})Errors: 404 if the trigger does not exist.
List triggers
GET /v1/triggers - returns {"items": [...], "total": <count>}. This list is not paginated; every matching trigger is returned.
Query parameters (both optional filters): kind (delayed, scheduled, or webhook) and enabled (boolean).
curl "https://your-host/v1/triggers?kind=scheduled&enabled=true" \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get("https://your-host/v1/triggers",
headers={"Authorization": f"Bearer {token}"},
params={"kind": "scheduled", "enabled": True})
page = r.json()
triggers = page["items"]const r = await fetch("/v1/triggers?kind=scheduled&enabled=true", {
headers: {"Authorization": `Bearer ${token}`}
})
const {items, total} = await r.json()Update a trigger
PUT /v1/triggers/{trigger_id} - partial update; returns 200 OK. Only the fields you send change; omitted fields keep their current value. The body accepts name, description, enabled, and config (the slug is immutable and is not part of the update body). A config that changes the trigger's kind is rejected with 409.
curl -X PUT https://your-host/v1/triggers/daily-report \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Daily Report (updated)",
"config": {"kind": "scheduled", "cron": "0 9 * * *", "timezone": "UTC"},
"enabled": true
}'import httpx
r = httpx.put(
"https://your-host/v1/triggers/daily-report",
headers={"Authorization": f"Bearer {token}"},
json={
"name": "Daily Report (updated)",
"config": {"kind": "scheduled", "cron": "0 9 * * *", "timezone": "UTC"},
"enabled": True,
},
)await fetch("/v1/triggers/daily-report", {
method: "PUT",
headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
body: JSON.stringify({
name: "Daily Report (updated)",
config: {kind: "scheduled", cron: "0 9 * * *", timezone: "UTC"},
enabled: true
})
})Errors: 404 if not found; 409 if the config changes the trigger's kind.
Delete a trigger
DELETE /v1/triggers/{trigger_id} - cascades to all subscriptions. Returns 204 No Content.
curl -X DELETE https://your-host/v1/triggers/daily-report \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.delete("https://your-host/v1/triggers/daily-report",
headers={"Authorization": f"Bearer {token}"})await fetch("/v1/triggers/daily-report", {
method: "DELETE",
headers: {"Authorization": `Bearer ${token}`}
})Fire a trigger now
POST /v1/triggers/{trigger_id}/fire_now - synchronously executes all enabled subscriptions for this trigger regardless of schedule. Returns 200 OK.
Response fields: skipped (boolean), fire_id (string), results (array of per-subscription outcomes, each with subscription_id, ok, skipped, and either artefact_id on success or error_code plus error_message on failure).
curl -X POST https://your-host/v1/triggers/daily-report/fire_now \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.post(
"https://your-host/v1/triggers/daily-report/fire_now",
headers={"Authorization": f"Bearer {token}"},
)
fire = r.json()
print(fire["results"])const r = await fetch("/v1/triggers/daily-report/fire_now", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`}
})
const {skipped, results} = await r.json()Errors: 404 if the trigger does not exist.
Subscription object
{
"id": "sub-001",
"config": {
"kind": "agent_fresh_session",
"workspace_id": "ws-prod",
"agent_id": "daily-report-agent"
},
"enabled": true,
"parallelism": "skip",
"payload_template": null
}
| Field | Required | Type | Description |
|---|---|---|---|
config |
yes | SubConfig | Discriminated subscription config (see kinds below) |
description |
no | string | Optional description (max 2000 chars) |
enabled |
no | boolean | Default true; disabled subscriptions are skipped on fire |
parallelism |
no | string | "skip" (default) or "queue": behavior when a prior fire's action is still running |
payload_template |
no | string or null | Jinja-style template rendered into the action payload |
Subscription kinds:
agent_fresh_session: start a new agent session when the trigger fires:
| Field | Required | Description |
|---|---|---|
kind |
yes | "agent_fresh_session" |
workspace_id |
yes | Id of the workspace to run the session in |
agent_id |
yes | Id of the agent to run |
graph_fresh_session: start a new graph run:
| Field | Required | Description |
|---|---|---|
kind |
yes | "graph_fresh_session" |
workspace_id |
yes | Id of the workspace |
graph_id |
yes | Id of the graph |
chat_message: post a message into a chat:
| Field | Required | Description |
|---|---|---|
kind |
yes | "chat_message" |
chat_id |
yes | Id of the target chat |
parked_session: resume a session parked on subscribe_to_trigger. Created automatically by the engine when an agent calls the workspace_ext__subscribe_to_trigger yielding tool; rarely created directly via REST:
| Field | Required | Description |
|---|---|---|
kind |
yes | "parked_session" |
session_id |
yes | Id of the parked session |
tool_call_id |
yes | Tool call id of the subscribe_to_trigger invocation |
parked_at |
yes | ISO 8601 timestamp when the session parked |
Create a subscription
POST /v1/triggers/{trigger_id}/subscriptions - returns 201 Created.
curl -X POST https://your-host/v1/triggers/daily-report/subscriptions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"config": {
"kind": "agent_fresh_session",
"workspace_id": "ws-prod",
"agent_id": "daily-report-agent"
},
"enabled": true,
"parallelism": "skip"
}'import httpx
r = httpx.post(
"https://your-host/v1/triggers/daily-report/subscriptions",
headers={"Authorization": f"Bearer {token}"},
json={
"config": {
"kind": "agent_fresh_session",
"workspace_id": "ws-prod",
"agent_id": "daily-report-agent",
},
"enabled": True,
"parallelism": "skip",
},
)
assert r.status_code == 201const r = await fetch("/v1/triggers/daily-report/subscriptions", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
body: JSON.stringify({
config: {
kind: "agent_fresh_session",
workspace_id: "ws-prod",
agent_id: "daily-report-agent"
},
enabled: true,
parallelism: "skip"
})
})Errors: 404 if the trigger does not exist; 422 if the config is invalid.
List subscriptions
GET /v1/triggers/{trigger_id}/subscriptions - returns a page of subscription objects. One-shot parked_session subscriptions are removed automatically once consumed.
curl https://your-host/v1/triggers/daily-report/subscriptions \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get(
"https://your-host/v1/triggers/daily-report/subscriptions",
headers={"Authorization": f"Bearer {token}"},
)
subs = r.json()["items"]const r = await fetch("/v1/triggers/daily-report/subscriptions", {
headers: {"Authorization": `Bearer ${token}`}
})
const {items} = await r.json()Delete a subscription
DELETE /v1/triggers/{trigger_id}/subscriptions/{subscription_id} - returns 204 No Content.
curl -X DELETE https://your-host/v1/triggers/daily-report/subscriptions/sub-001 \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.delete(
"https://your-host/v1/triggers/daily-report/subscriptions/sub-001",
headers={"Authorization": f"Bearer {token}"},
)await fetch("/v1/triggers/daily-report/subscriptions/sub-001", {
method: "DELETE",
headers: {"Authorization": `Bearer ${token}`}
})Errors note
Trigger and subscription errors raised by this router (404 not-found, 409 trigger_slug_conflict / trigger_kind_immutable, and the 422 codes cron_invalid, timezone_invalid, parked_session_only_from_yield, not_a_webhook_trigger) use a {"detail": {"code": "<error_code>", "message": "..."}} envelope; dispatch on response.json()["detail"]["code"]. Request-body validation failures (malformed JSON, missing required field, wrong type) are caught earlier and returned in the RFC 7807 ProblemDetails envelope (type /errors/validation-error, plus extensions.errors). See the REST API overview for details.