Triggers and Schedules

A loop’s triggers say what starts a run. A conversation is the default: a person starts the run through a delivery. The other triggers start a headless run, a run with no user.

TriggerWhat starts a run
conversationA person, through the embed, the SDK or your own interface.
scheduleA time, written as a five-field cron, in the trigger’s timezone (UTC when it names none).
event with source: webhookA signed POST from your own system.
event with source: collectionA record written to one of the loop’s collections.
event with source: pageA reader pressing a button on a page’s row.
apiYour server, through the run API.

A person’s chat needs a conversation trigger. Publish refuses a loop whose delivery.kinds holds embed or sdk and whose triggers are all headless, and a page refuses to embed such a loop. Add {kind: conversation} to triggers, or remove embed and sdk from delivery.kinds. The console’s Chat, and an AI tool’s run_loop, still start any loop: they run as you, so you can test a schedule loop there before it runs on its own. If a loop loses its conversation trigger after a page embeds it, the page’s chat tells a reader This chat isn’t available right now. The run API answers 403 with the reason no_conversation_trigger, and names the trigger to add only to the loop’s key or your own preview of the page (the browser console shows that sentence too).

Rules for a run with no user

  • It has a subject and no user. The subject is what the run works on, read from the payload or fixed by the schedule. The per-user cap counts per subject.
  • It cannot ask anyone a question. A run with no user is never offered ask_user. A step can still wait for your decision.
  • It must send its result somewhere. Publish refuses a headless loop with no outputs, except one whose only headless trigger is a page button: its result is the changed row.
  • Its input is data, never instructions. A webhook body, a record or a schedule’s subject reaches the run as untrusted content.
  • A flood is stopped before a model runs. concurrency caps how many runs one trigger may have in flight. It is required on event and api triggers, and optional on schedule.
  • The entry agent must be able to read the payload. Publish refuses the trigger otherwise.

On a schedule

On Schedules, say what should run and when under What should run, and when?: choose a loop under Which loop (or A new loop), type something like “Every Monday at 9am, summarize what’s on example.com”, and press Ask LoopAssistant. LoopAssistant opens in the Assistant panel with your words, and adds the schedule to the loop as a change for you to approve and then publish on the loop’s page. On a loop with a change already waiting, the schedule adds to that change. LoopAssistant sets the schedule in your browser’s time zone, so it keeps your local time when daylight saving starts or ends, and tells you the time it set. The schedule shows on Schedules once the loop is published.

In the loop’s own text, a schedule is a trigger:

triggers:
  - kind: schedule
    cron: "0 9 * * 1"        # 09:00 every Monday
    timezone: America/Los_Angeles
    subject: weekly_digest

timezone is optional and takes a name from the IANA time zone list, such as America/Los_Angeles or Europe/London. The hours in cron are in that zone, so 09:00 stays 09:00 there when daylight saving starts or ends. Without timezone, the hours are in UTC, and a local time moves by an hour at each change. Publishing refuses a name the list does not hold, and names the spelling it expects (America/Los_Angeles, not america/los_angeles).

Publishing the version installs the schedule, and the loop’s detail on Loops shows it. A new version moves the schedule to that version. Archiving the loop removes it.

Schedules lists every schedule in the account. For each one you can Run now, pause, resume or delete it, and read its recent runs with what each cost. A run that woke an agent says so and links to the run it woke.

Each firing starts one run, with the subject the trigger names.

From a signed webhook

triggers:
  - kind: event
    source: webhook
    subject: $.lead_id       # where the payload names what the run works on
    concurrency: 5
    secret_ref: ssm:/…       # where the signing secret lives

A loop can declare one webhook trigger. If secret_ref is wrong, publish refuses it and names the path it expects.

Once the version is published, the loop’s page shows the webhook’s address with Copy the address, and a form to store its secret. Every request is refused until the secret is stored. The value is never shown again, and storing a new one replaces it.

Sign each request with the Pio-Signature header: the same scheme as the platform’s outbound webhooks. Webhooks shows the header, the limits and every refusal.

From a new record

triggers:
  - kind: event
    source: collection
    collection: leads
    subject: $.lead_id
    concurrency: 2

Each record written to leads starts one run. The collection must be one the loop declares. Records that such a run writes start nothing, so collection triggers never chain. To do a second step, do it in the same run, or in a schedule that reads the collection.

When the trigger is at its concurrency cap, the write still lands, and its run ends at once as cancelled, in words that name the fix.

From a page button

triggers:
  - kind: event
    source: page
    collection: replies
    concurrency: 2

A page’s row button can run this loop on that row: actions=[Draft a reply: run draft_reply] on a {{collection replies}} block. See Buttons that run a loop. Each press starts one run, about that row. The collection must be one the loop declares, and the trigger takes no subject: the row is the subject, so the loop’s per-subject cap counts per row.

  • The run may change that row and no other. records_put refuses a write to any other row, and a new row, in words that name the row it may write. This holds whatever else the loop may write, because the reader’s right was to change that row.
  • The row reaches the run as untrusted data, as a webhook body does.
  • The rows it writes start nothing, as a collection trigger’s do.
  • When the trigger is at its concurrency cap, the press starts nothing and the reader is told to try again later.
  • It needs no outputs. The run’s result is its change to the pressed row, which the page shows. A loop that also has a schedule, webhook, collection or api trigger still needs an output for those runs.
  • Who may press the button is set on Collections, on the collection, as for any row button: a loop’s own collection too.

From your server

An api trigger lets your server start headless runs with POST /api/runs/batch, up to 25 at a time, under an API key. See Runs.