AgentNava · Build

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.

kindWho starts the turnYou give it
chata personnothing
schedulenobodya cron expression, in the timezone you name
intervalitselfseconds, and optionally maxRuns
webhookyour systemnothing. 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.kindChecks
stripethe Stripe-Signature header
githubthe X-Hub-Signature-256 header
shopifythe X-Shopify-Hmac-Sha256 header
slackthe X-Slack-Signature header, and answers Slack's one-time URL check
standardthe webhook-signature header of the Standard Webhooks spec
headera header you name, holding the secret you set
nonenothing: 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 nameEach firing runs in
conversationIdthat conversation, with its history and its drive. It also moves to the agent's latest version, so a fix you publish reaches it.
userIda new conversation for that end user, every time.
neitherone 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

And a turn is billed

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.