Skip to content
Go To Dashboard

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.

kindFires whenRequired field
schedule_cronon a recurring cron expressioncron (optional timezone)
schedule_onceonce, at a future timeat (ISO 8601)
eventevery time your tenant emits a matching event type through the events APIeventType
webhookevery time an external system POSTs a correctly signed request to a public hook URLnone: 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.

  • Your own code should start the agent at a moment it knows about (a lead was created, a job finished): use an event trigger and have that code call the events API.
  • An external system you control should start the agent by POSTing to a URL: use a webhook trigger. 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 with webhooksEnabled. 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.

Ask your coding agent to arm the deployed agent on an event type:

Using Sapiom MCP, make the deployed enrich-lead agent run every time we emit a lead.created event.

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).

Any process holding a Sapiom API key can emit an event for the tenant with POST /v1/workflows/events:

Terminal window
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]" }
}'
  • payload must be a JSON object. It becomes the top layer of the run input, folded over the trigger’s stored input (the payload wins on key conflicts), and is validated against the agent’s entry inputSchema before the run starts.
  • id is optional but recommended. Reposting the same id returns the original receipt and starts nothing new, so retries are safe. Omit it and every POST is a distinct event.
  • The response is 202 with receiptId, an outcome (matched or unmatched), duplicate, and one fire id per trigger the event matched. unmatched is not an error; it means no active trigger subscribes to that type.
  • A 202 confirms the event was received, not that a run started. A fire whose payload fails the entry inputSchema is marked failed and starts nothing. To see which run a fire started, inspect the trigger (below): each recent fire carries the executionId it 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.

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:

FieldValue
definitionIdid of the agent whose run failed
slugthat agent’s slug
definitionNamethat agent’s display name
executionIdthe failed run
failedStepname of the last failed step, or null
attemptthat step’s attempt number, 0-based, or null
faultClassinfra, workload, or null
startedAtwhen the run started (ISO 8601)
finishedAtwhen 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.

Using Sapiom MCP, make the deployed inbound-message agent 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.

Every POST to the hook URL must carry three headers:

HeaderValue
X-Sapiom-TimestampUnix epoch milliseconds, digits only. Requests more than five minutes from server time are rejected.
X-Sapiom-Event-IdA sender-chosen delivery id, [A-Za-z0-9_-]{1,128}. A repeat is deduplicated, so retrying with the same id is safe.
X-Sapiom-SignatureHMAC-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).

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.

  1. { "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; graceUntil on the trigger says when it ends.

  2. { "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.

  3. { "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.

sapiom_dev_agents_schedule_inspect and sapiom_dev_agents_schedule_cancel cover all four kinds.

  • Listing by definition returns each trigger’s kind, status, eventType (event), and publicId, secretVersion, graceUntil, revokedAt (webhook). The secret is never included; it is not stored.
  • Inspecting by scheduleId adds recentFires. An event or webhook fire has no scheduledFor; it carries the receiptId of the inbound delivery instead, alongside the executionId it started. Follow that id with sapiom_dev_agents_inspect.
  • Cancelling sets status to disabled. An event trigger stops matching, and a webhook answers 401 from 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}.