primer docs

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 == 201
const 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, bad fire_at format)
  • 409 - a trigger with this slug already 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 == 201
const 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.