Skip to content

Triggers

A trigger step is where a run starts. The event that starts it becomes the run’s first carried item. A workflow can have several triggers, for example a schedule for the nightly batch and an Event trigger for testing by hand. Each trigger mode is told apart by how its event arrives, so they never compete.

Mode Builder entry Starts a run when
Event Event a workflow.start event targeted at the workflow arrives
Schedule Schedule the vault’s clock reaches a cron boundary
Webhook Webhook another system calls the workflow’s URL with a key
Connector the connector’s name, under Triggers a message arrives through a vault connection

Every trigger fires only in vaults the workflow is connected to. See Connect to a vault.

Event

The default trigger. A run starts when a workflow.start event targeted at the workflow is published into the scope the workflow is connected on. The event’s payload is the first item.

Three things publish it:

  • the Run button on the workflow’s Executions tab in ROC;
  • an app, through the vault SDK;
  • another agent or workflow.
// From an app, with @cef-ai/vault-sdk
await vault.scope("default").publish({
type: "workflow.start",
context: `ticket-${ticketId}`, // one run per context
target: "<agentServicePubkey>:ticket-triage",
payload: { ticketId, text, customer },
});

The context names the run: a second workflow.start in a context that already has a run is ignored. Use a fresh context per run, or a deterministic one (such as the ticket id) to make retries safe.

Param Builder label What it does
eventType Event type Keep workflow.start.
sample Example payload JSON text that prefills the Run dialog’s What starts it field.
{ id: "ticket", kind: "trigger", params: { eventType: "workflow.start", sample: JSON.stringify({ ticketId: "T-1", text: "I was charged twice" }) } }

A workflow.start payload may also carry participants (see People in workflows) and from: "<stepId>" to start the run at a chosen step instead of the trigger.

Schedule

The vault starts a run on a cron schedule, once per connected vault: a workflow connected to three vaults fires three times at each boundary, each run in its own vault.

In the Builder

Add Schedule from Triggers and choose a cadence under Every: Every N minutes, Every N hours, Daily, Weekly, Monthly, or Custom cron. Pick a Timezone and, under Input — sent with every fire, the fields every run receives. The panel shows the next fire time. Deploy installs the schedule.

The Schedule trigger: cadence, timezone, and the next runs

In code

A schedule is a trigger step with mode: "schedule" plus an entry in schedules whose id is that step’s id:

defineWorkflow({
id: "ticket-digest",
version: "0.1.0",
nodes: [
{ id: "nightly", kind: "trigger", params: { mode: "schedule" } },
// …
],
edges: [/* … */],
schedules: [
{
id: "nightly",
cron: "0 6 * * 1-5",
timezone: "Europe/Amsterdam",
eventType: "workflow.start",
payload: { window: "24h" },
},
],
});
Field What it is
id The schedule trigger’s step id. Stable across versions: renaming it removes one schedule and adds another.
cron Five fields (minute hour day-of-month month day-of-week), or a descriptor such as @daily. No seconds field.
timezone IANA name. UTC when omitted.
eventType workflow.start.
payload Merged into every fire’s payload.

What a run receives

The declared payload plus a schedule block:

{
"window": "24h",
"schedule": {
"id": "nightly",
"firedAt": "2026-10-08T04:00:00Z",
"previousFiredAt": "2026-10-07T04:00:00Z",
"cron": "0 6 * * 1-5",
"timezone": "Europe/Amsterdam"
}
}

Behavior

  • A fire that is missed (the vault was not running at that time) is skipped, never backfilled. The schedule continues from the next boundary.
  • A schedule with an invalid cron or timezone does not stop the deploy; it stays silent and records the error on the schedule.
  • Pressing Run on a workflow whose only trigger is a schedule enters at the schedule trigger, so you can test it by hand.

Schedule limits

What Limit
Granularity 1 minute: a 5-field cron, or an @hourly / @daily-style descriptor. An expression with a seconds field is refused.
Late fire A fire more than 4 minutes late is skipped. Make each run cover “since the last run”, not “the last minute”.

Webhook

Another system calls the workflow’s URL with a key, and each call starts a run. The caller can get an answer at once or wait for the run’s result.

Set it up

  1. Add Webhook from Triggers. Choose Respond: Immediately or Wait for result, and a Timeout (seconds) for waiting calls.
  2. Deploy. The trigger’s panel shows the Webhook URL for the vault the workflow runs in.
  3. Under Keys, choose Create key, give it a Name and an optional expiry. Copy the key: it is shown once.

A run started with a key runs as the member who created the key. If that member loses write access to the workflow’s scope, calls are refused. Revoke a key from the same panel; revocation is immediate.

The Webhook URL panel with one key and the Copy as menu

Call it

Terminal window
curl -X POST "$WEBHOOK_URL?wait=true" \
-H "Authorization: Bearer $CEF_WEBHOOK_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ticket-T-1042" \
-d '{"ticketId": "T-1042", "text": "The export button does nothing"}'

The Copy as menu on the URL gives the same call as cURL, JavaScript, Python, a prompt for an AI agent, or a tool definition.

Request part Rules
Authorization Bearer <key>.
Body A JSON object becomes the run’s item. A JSON array arrives as { "body": [...] }. An empty body is {}. Anything else is refused.
?wait=true / ?wait=false Overrides the trigger’s Respond setting for this call.
Idempotency-Key Optional, up to 255 bytes. Repeating a call with the same key and the same value within 5 minutes returns the same run instead of starting another.

The run’s item is the body plus a webhook block: { id, endpointId, keyId, keyName, receivedAt }. A body field named webhook is overwritten.

Responses

Situation Status Body
Respond immediately 202 { "status": "accepted", "runId", "context", "statusUrl" }
Waiting, run finished 200 { "status": "completed", "runId", "context", "output" }
Waiting, run failed 200 { "status": "failed", "runId", "context", "error" }
Waiting, run parked on a person 202 { "status": "awaiting_input", "runId", "context", "statusUrl" }
Waiting, timeout reached 202 { "status": "running", "runId", "context", "statusUrl" }

Poll statusUrl with the same Authorization header until status is completed or failed. output is what the workflow’s Result step declares; without one, it is the item the run ended with.

Errors

Error bodies are { "error": { "code", "message" } }.

Status Code Cause
400 BAD_REQUEST Body is not JSON, or an invalid wait or Idempotency-Key.
401 WEBHOOK_UNAUTHORIZED Unknown URL, or a missing, wrong, revoked, or expired key. Always the same answer, whatever the cause.
403 WEBHOOK_CREATOR_NO_ACCESS The key’s creator can no longer write the workflow’s scope.
413 PAYLOAD_TOO_LARGE Body over 1 MiB.
429 RATE_LIMITED Over the key’s rate limit. Wait Retry-After seconds.
503 INGEST_UNAVAILABLE The run could not be started. Retry.

Webhook limits

What Limit
Body 1 MiB
Wait timeout default 30 s, maximum 120 s
Rate per key 60 calls per minute, bursts of 10
Idempotency-Key 255 bytes; deduplicated for 5 minutes
Key name 100 characters

The URL and keys stay the same across redeploys as long as the trigger’s step id does not change. Removing the trigger deletes its URL and keys.

Webhook URLs are installed by Deploy in the Builder from the graph’s Webhook triggers; defineWorkflow has no webhook declaration.

Connector

A message arriving through a vault connection (a Slack message, a Telegram message) starts a run. Connections are set up by the vault owner on the Connectors page; see Connectors.

In the Builder

Under Triggers, pick the connector (for example Slack), then its event. Choose the {System} connection and, optionally, a filter such as a channel; leave it empty to start on every message the connection receives. Deploy grants the workflow access to that connection’s event.

In code

{
id: "slack-in",
kind: "trigger",
params: { mode: "connector", connection: "<connectionId>", event: "message.received" },
}

connection and event are required. Several connector triggers can share one workflow; a message enters at the first trigger whose connection and event match.

What a run receives

The connector’s event fields, plus connector, event, and identity. For Slack message.received: teamId, channelId, userId, text, ts, threadTs. For Telegram: chatId, chatType, userId, username, text, messageId.

identity describes the external sender ({ connector, externalId, displayName?, resolution }). It is not a vault member: do not use it to authorize anything.

Each Slack thread or Telegram chat is its own context, so the first message in it starts a run.