Trigger an agent
A trigger is a persisted cloud object attached to an already-deployed agent by its slug. Each fire starts an independent production run of that agent. Sapiom accepts four trigger kinds, and all four are created, inspected, and cancelled with the same Sapiom MCP tools.
kind | Fires when | Required field |
|---|---|---|
schedule_cron | on a recurring cron expression | cron (optional timezone) |
schedule_once | once, at a future time | at (ISO 8601) |
event | every time your tenant emits a matching event type through the events API | eventType |
webhook | every time an external system POSTs a correctly signed request to a public hook URL | none: the create response returns the URL and a shown-once secret |
The two schedule kinds are covered in Schedule an agent. This guide covers event and webhook, and when a webhook trigger is the right receiver.
Choose the right receiver
Section titled “Choose the right receiver”- Your own code should start the agent at a moment it knows about (a lead was created, a job finished): use an
eventtrigger and have that code call the events API. - An external system you control should start the agent by POSTing to a URL: use a
webhooktrigger. The sender computes Sapiom’s HMAC signature with the secret returned at create time. - A third party with its own signature scheme (Slack, Meta / WhatsApp, Stripe, GitHub) should start the agent: those senders cannot produce Sapiom’s HMAC, so give them an App Link
/hook/*receiver withwebhooksEnabled. The app verifies the provider’s signature and then emits an event or starts the run through the API. A small translator service that verifies the provider’s signature and re-signs into a webhook trigger works the same way. - A paused run should continue when a callback arrives: that is a signal, not a trigger. Triggers start new runs; signals resume existing ones.
Before creating any trigger, deploy the agent and confirm it has a ready build. A fire cannot start a run without one.
Event triggers
Section titled “Event triggers”Create the trigger
Section titled “Create the trigger”Ask your coding agent to arm the deployed agent on an event type:
Using Sapiom MCP, make the deployed
enrich-leadagent run every time we emit alead.createdevent.
The sapiom_dev_agents_schedule input is:
{ "definition": "enrich-lead", "kind": "event", "eventType": "lead.created", "input": { "source": "crm" }}Event types are lowercase segments joined by single dots ([a-z0-9_]+(\.[a-z0-9_]+)*, up to 255 characters). The sapiom.* namespace is reserved for platform events: no tenant can emit a sapiom.* type, and the only one a trigger can subscribe to is sapiom.run.failed. Types are matched exactly and never normalized, so Lead.Created is rejected rather than lowercased. A trigger on a type nobody emits is valid and simply never fires.
REST equivalent: POST /v1/workflows/definitions/{slug}/triggers with the same body minus definition (it is the {slug} in the path; the API rejects unknown body fields with a 400).
Emit an event
Section titled “Emit an event”Any process holding a Sapiom API key can emit an event for the tenant with POST /v1/workflows/events:
curl -X POST https://api.sapiom.ai/v1/workflows/events \ -H "x-api-key: $SAPIOM_API_KEY" \ -H "content-type: application/json" \ -d '{ "type": "lead.created", "id": "crm-evt-8f2a", "payload": { "leadId": "L-1042", "email": "[email protected]" } }'payloadmust be a JSON object. It becomes the top layer of the run input, folded over the trigger’s storedinput(the payload wins on key conflicts), and is validated against the agent’s entryinputSchemabefore the run starts.idis optional but recommended. Reposting the sameidreturns the original receipt and starts nothing new, so retries are safe. Omit it and every POST is a distinct event.- The response is
202withreceiptId, anoutcome(matchedorunmatched),duplicate, and one fire id per trigger the event matched.unmatchedis not an error; it means no active trigger subscribes to that type. - A
202confirms the event was received, not that a run started. A fire whose payload fails the entryinputSchemais marked failed and starts nothing. To see which run a fire started, inspect the trigger (below): each recent fire carries theexecutionIdit started.
The route is rate limited per tenant and per source IP. A 429 carries a Retry-After header; back off and resend with the same id.
From a coding agent, sapiom_dev_agents_emit_event (@sapiom/mcp 0.18 or later) sends the same request with the same type, payload, and id.
React to a failed run: sapiom.run.failed
Section titled “React to a failed run: sapiom.run.failed”The platform emits sapiom.run.failed when a run of an agent in your tenant ends failed. It is the only sapiom.* type a trigger can subscribe to. The events API (and sapiom_dev_agents_emit_event) refuses every sapiom.* type, this one included, so a tenant cannot forge it.
Subscribe a watchdog agent with an event trigger:
{ "definition": "failure-watchdog", "kind": "event", "eventType": "sapiom.run.failed"}Each fire’s payload describes the failed run:
| Field | Value |
|---|---|
definitionId | id of the agent whose run failed |
slug | that agent’s slug |
definitionName | that agent’s display name |
executionId | the failed run |
failedStep | name of the last failed step, or null |
attempt | that step’s attempt number, 0-based, or null |
faultClass | infra, workload, or null |
startedAt | when the run started (ISO 8601) |
finishedAt | when the run ended (ISO 8601), or null |
No error text is included. To read what went wrong, inspect the failed run with sapiom_dev_agents_inspect and its executionId.
- The event is never delivered to the failing agent’s own triggers, so a watchdog does not fire on its own failure.
- A run it started never emits it again, so watchdogs cannot chain.
- Declare the payload fields the watchdog reads in its entry
inputSchema; the payload is validated against it like any event payload. - Delivery is best effort.
Webhook triggers
Section titled “Webhook triggers”Create the trigger
Section titled “Create the trigger”Using Sapiom MCP, make the deployed
inbound-messageagent run whenever our telephony gateway POSTs a message to us.
{ "definition": "inbound-message", "kind": "webhook", "input": { "channel": "sms" }}The response carries three things you will not see again together:
webhook.url: the public ingress,https://api.sapiom.ai/v1/workflows/hooks/{publicId}.webhook.secret: the signing secret. It is derived on demand and never stored, so it is shown once. If it is lost, rotate it.webhook.signing: the scheme below, restated in the tool result so the sender can be configured without leaving the session.
REST equivalent: POST /v1/workflows/definitions/{slug}/triggers with { "kind": "webhook" } (plus input if you want one; definition is the {slug} in the path, not a body field); the url and secret fields are on the create response only.
Sign each request
Section titled “Sign each request”Every POST to the hook URL must carry three headers:
| Header | Value |
|---|---|
X-Sapiom-Timestamp | Unix epoch milliseconds, digits only. Requests more than five minutes from server time are rejected. |
X-Sapiom-Event-Id | A sender-chosen delivery id, [A-Za-z0-9_-]{1,128}. A repeat is deduplicated, so retrying with the same id is safe. |
X-Sapiom-Signature | HMAC-SHA256(secret, "<timestamp>.<eventId>.<raw body>"), lowercase hex. |
The body is the exact bytes that were signed. Send a JSON object of up to 1 MiB; it becomes the run input, folded over the trigger’s stored input.
import { createHmac, randomUUID } from "node:crypto";
const body = JSON.stringify({ from: "+15551234567", text: "hello" });const timestamp = String(Date.now());const eventId = randomUUID();const signature = createHmac("sha256", process.env.HOOK_SECRET) .update(`${timestamp}.${eventId}.${body}`) .digest("hex");
await fetch(process.env.HOOK_URL, { method: "POST", headers: { "content-type": "application/json", "X-Sapiom-Timestamp": timestamp, "X-Sapiom-Event-Id": eventId, "X-Sapiom-Signature": signature, }, body,});A verified request answers 202 with { "receiptId": "...", "duplicate": false }. A bad signature, a stale timestamp, a revoked hook, and an unknown URL all answer the same 401, so a probe cannot learn which hooks exist. A 400 means the signature verified but the body was unusable (for example, not a JSON object).
Rotate or revoke the secret
Section titled “Rotate or revoke the secret”Two distinct gestures cover the secret’s lifecycle. Both are available through sapiom_dev_agents_schedule_secret (@sapiom/mcp 0.15+) and as REST routes.
-
Rotate (planned hygiene)
Section titled “Rotate (planned hygiene)”{ "scheduleId": "<trigger-id>", "action": "rotate" }REST:
POST /v1/workflows/triggers/{id}/secret/rotate. The response carries the new secret (shown once) and the hook URL, so the sender can be reconfigured in one step. The previous secret keeps verifying for a 24-hour grace period;graceUntilon the trigger says when it ends. -
Complete the rotation
Section titled “Complete the rotation”{ "scheduleId": "<trigger-id>", "action": "complete_rotation" }REST:
DELETE /v1/workflows/triggers/{id}/secret/previous. Once the sender has moved to the new secret, end the grace early so the old secret stops verifying immediately. -
Revoke (compromise)
Section titled “Revoke (compromise)”{ "scheduleId": "<trigger-id>", "action": "revoke" }REST:
POST /v1/workflows/triggers/{id}/secret/revoke. The hook rejects every request from then on and any open grace ends with it. Revocation is final; create a new webhook trigger to hand the sender a fresh URL and secret.
Inspect and cancel any trigger
Section titled “Inspect and cancel any trigger”sapiom_dev_agents_schedule_inspect and sapiom_dev_agents_schedule_cancel cover all four kinds.
- Listing by
definitionreturns each trigger’skind,status,eventType(event), andpublicId,secretVersion,graceUntil,revokedAt(webhook). The secret is never included; it is not stored. - Inspecting by
scheduleIdaddsrecentFires. An event or webhook fire has noscheduledFor; it carries thereceiptIdof the inbound delivery instead, alongside theexecutionIdit started. Follow that id withsapiom_dev_agents_inspect. - Cancelling sets
statustodisabled. An event trigger stops matching, and a webhook answers401from then on. Cancellation is final; recreate the trigger to re-arm it (a recreated webhook has a new URL and secret).
REST: GET /v1/workflows/definitions/{slug}/triggers, GET /v1/workflows/triggers/{id}, DELETE /v1/workflows/triggers/{id}.
© 2026 Sapiom, Inc.