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.
| Trigger | What starts a run |
|---|---|
conversation | A person, through the embed, the SDK or your own interface. |
schedule | A time, written as a five-field cron, in the trigger’s timezone (UTC when it names none). |
event with source: webhook | A signed POST from your own system. |
event with source: collection | A record written to one of the loop’s collections. |
event with source: page | A reader pressing a button on a page’s row. |
api | Your 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.
concurrencycaps how many runs one trigger may have in flight. It is required oneventandapitriggers, and optional onschedule. - 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_putrefuses 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
concurrencycap, 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 aschedule,webhook,collectionorapitrigger 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.