Triggers
Chat is always on. Set a trigger when something else should start a run.
What starts a run
Chat is always one of them and cannot be removed, so most agents never set this. You set triggers when you want one of the others as well.
kind | Who starts the turn | You give it |
|---|---|---|
chat | a person | nothing |
schedule | nobody | a cron expression, in the timezone you name |
interval | itself | seconds, and optionally maxRuns |
webhook | your system | nothing. You get a URL back |
await agent.setTriggers([
{ kind: 'chat' },
{
kind: 'schedule',
cron: '0 8 * * 1',
timezone: 'America/Los_Angeles',
message: 'Summarise last week\'s failed payments over 500 dollars.',
},
]);message is what the agent is asked
A run with no human attached still has to be told something, or it wakes with nothing in front
of it. Required on schedule and interval. Optional on
webhook, where it is prepended to your payload so the agent knows what it is looking
at before it sees it.
Webhooks
Setting a webhook trigger hands back a URL to POST to. Paste it into the system that sends the events: Stripe, Shopify, Zapier, or your own code.
const updated = await agent.setTriggers([
{ kind: 'chat' },
{ kind: 'webhook', message: 'A support ticket arrived. Triage it.' },
]);
const url = updated.data?.triggers.find((t) => t.kind === 'webhook')?.url;The URL stays the same across every later setTriggers() and update(), so a sender you set up once keeps working. An agent has one webhook.
The secret part of the URL is the only check on who may call it, so treat the URL like a password. If it leaks, replace it. The old URL stops working at once:
const newUrl = await agent.rotateWebhookUrl();Check who sent it
Stripe, GitHub, Shopify and Slack sign every delivery with a secret they show you when you add the URL there. Give the webhook that secret, and a delivery whose signature does not match is refused before the agent runs. Tools that do not sign (Zapier, Make, n8n, your own code) can add a header instead.
await agent.setTriggers([
{ kind: 'chat' },
{ kind: 'webhook', message: 'A payment arrived.', verify: { kind: 'stripe', secret: 'whsec_...' } },
]);
// Or a header your own sender adds:
// verify: { kind: 'header', header: 'X-Webhook-Secret', secret: '...' }verify.kind | Checks |
|---|---|
stripe | the Stripe-Signature header |
github | the X-Hub-Signature-256 header |
shopify | the X-Shopify-Hmac-Sha256 header |
slack | the X-Slack-Signature header, and answers Slack's one-time URL check |
standard | the webhook-signature header of the Standard Webhooks spec |
header | a header you name, holding the secret you set |
none | nothing: the URL is the only lock |
The secret is stored encrypted and never read back. Leaving verify out keeps what the webhook has; { kind: 'none' } turns the check off.
Retries run once
Senders retry when they are not sure a delivery arrived. A delivery with the same id as one already accepted (Stripe's event id, GitHub's X-GitHub-Delivery, the webhook-id header, or an Idempotency-Key header you send) answers 200 and does not run the agent again. See what arrived with await agent.webhookDeliveries(), and try the webhook with await agent.sendTestWebhook(), which runs the agent on a sample event and is charged as a turn.
Where a run lands
A schedule or webhook runs in a conversation. What you name decides which one:
| You name | Each firing runs in |
|---|---|
conversationId | that conversation, with its history and its drive. It also moves to the agent's latest version, so a fix you publish reaches it. |
userId | a new conversation for that end user, every time. |
| neither | one conversation the trigger keeps for itself, continued at every firing. |
const conversation = await agent.start({ endUserId: 'ops-app' });
await agent.setTriggers([
{ kind: 'chat' },
{
kind: 'schedule',
cron: '0 15 * * *',
message: 'Refresh the dashboard.',
conversationId: conversation.id,
},
]);One end user can have many conversations, so a userId cannot pick one, and the
platform does not guess. To keep adding to the same conversation, name it. It must be a
conversation of this agent, started for one of your end users; anything else is refused when you
set the trigger. Name one or the other, never both. If the conversation is deleted later, each
firing fails and says so on the schedule, and no other conversation is opened in its place.
Every firing is a turn
An interval fires at most once a minute (a shorter seconds is
raised to 60), and with no maxRuns it is a bill that does not stop. Set
maxRuns unless you mean it to run forever.
Out of credit, a firing is skipped instead of run, and it does not use up one of your
maxRuns. The loop carries on at its usual pace, so it picks up again once credit
is added.
Triggers belong to the version, like everything else that says what the agent is. A trigger's
message is a prompt, so rolling an agent back brings its schedule with it. Only one
schedule fires at a time: the scheduler reads the newest version. A trigger starts a
new run and is never read by a conversation already in progress.
An interval follows the agent. Deleting the agent stops it, and so does a
setTriggers() without it. Changing its seconds, maxRuns or
message starts it again from its first run. Publishing anything else leaves it
alone: it keeps its count and its next firing, and one that has used up its
maxRuns stays finished.