Scheduled triggers
Use cron-schedule event triggers to run periodic work (daily summaries, stale-task reminders, trust decay audits) without external schedulers.
This page is for people running their own workspace or using the API. If you use a workspace created at syncropel.com, start with Your hosted workspace.
What they are
Event triggers in Syncropel are stored as LEARN records on
th_engine_config with body.topic = "event_trigger". They come in
two flavors:
- Record-matching: fire when a record matching a CEL expression is ingested (documented in Routing rules)
- Schedule-based (this guide): fire when a cron schedule elapses
This guide is about the schedule flavor: the "give me a daily summary at 9am" pattern. No external scheduler needed; the engine's CRON loop evaluates schedules on every tick.
Prerequisites
- Running
spl serveinstance - An actor DID to dispatch to (typically
did:sync:agent:<role>) - The actor must have a registered adapter (see Actors and adapters)
Quickstart: daily summary
Dispatch your dev agent every morning at 9am to summarize yesterday's work:
spl config add-trigger \
--name "daily-summary" \
--schedule "0 9 * * *" \
--target "did:sync:agent:dev" \
--goal "Summarize yesterday's commits + open tasks, post as KNOW on th_daily_summary" \
--cooldown 300 \
--budget 0.5 \
--timeout 600Cron format is standard: minute hour day month weekday. The above
reads "at 9am UTC every day". The instance evaluates each cron on every
tick_interval_ms (default 1s), so firing resolution is ~1s.
Verify the trigger
spl config list-triggersLook for your new entry. Confirm it's enabled: true and the schedule
matches. To see past firings:
spl config trigger-history --limit 10Each row shows the trigger name, firing time, thread id produced, and cost.
Pause, resume, remove
spl trigger is the verb group for a trigger's life after it exists.
list and show read the LIVE loaded set, which is not always the same as
the records: a trigger whose schedule, timezone or idempotency declaration is
invalid is REFUSED at load and marked REFUSED in the listing rather than
silently behaving like a live one.
spl trigger list # the live set, with refusals marked
spl trigger show <name> # the whole definition
spl trigger pause <name> # stops firing; keeps the whole definition
spl trigger resume <name> # puts it back exactly as it was
spl trigger remove <name> # removes it for goodremove writes a TOMBSTONE record rather than deleting anything: the ledger is
append-only, so the removal is a fact on it like every other, and the original
declaration stays as history. That has one consequence worth knowing before you
reach for it: declaring the same name again brings the trigger back, which
is what latest-wins means on an append-only log.
Removing a trigger that was REFUSED at load works too, and is the reason the verb exists: before it, a trigger that could not load also could not be taken out of the listing.
Common patterns
Stale-task reminder
Fire every 4 hours, poke any tasks that have been in_progress >48h:
spl config add-trigger \
--name "stale-task-check" \
--schedule "0 */4 * * *" \
--target "did:sync:agent:ops" \
--goal "For each task where status == in_progress and age > 48h, emit KNOW with reminder" \
--budget 0.2 \
--timeout 300Trust decay audit
Weekly Wilson-LB-dropped actors get surfaced for operator review:
spl config add-trigger \
--name "weekly-trust-audit" \
--schedule "0 10 * * MON" \
--target "did:sync:agent:auditor" \
--goal "List actors whose trust.code.effective dropped >0.1 in the past 7 days" \
--budget 0.3Fleet heartbeat verification
Every 15 minutes, fail-fast if any fleet peer hasn't posted health:
spl config add-trigger \
--name "fleet-peer-liveness" \
--schedule "*/15 * * * *" \
--target "did:sync:agent:ops" \
--goal "Alert if any fleet peer has not heartbeat in 5 minutes" \
--budget 0.1 \
--timeout 180Cron expression cheatsheet
| Spec | Fires |
|---|---|
0 9 * * * | 9:00am every day (UTC, unless the trigger names a timezone) |
*/15 * * * * | Every 15 minutes |
0 */4 * * * | Every 4 hours on the hour |
0 10 * * MON | 10:00am every Monday |
0 0 1 * * | Midnight on the 1st of every month |
30 8 * * MON-FRI | 8:30am on weekdays |
Schedules are UTC unless the trigger names a timezone (an IANA name such as Europe/London).
Rate limiting
--cooldown <secs> throttles a trigger's firings. Useful if the CEL
schedule somehow produces double fires, or if you want a defensive
minimum gap.
Actors that run unattended
A scheduled actor's definition can say what its runs may write and what they owe:
work_loop.writesnames the threads its runs may write, andwork_loop.write_kindsthe record kinds. Configuration and authority threads and kinds are refused when the actor is defined, so a scheduled run can never arm a trigger or change a workspace's limits.work_loop.revertsays how the actor's effects are undone. An actor that holds a tool with an effect outside the workspace (an HTTP call, for example) needs it to start.work_loop.contractandwork_loop.promisesstate what a run commits to and produces. A trigger'sdispatchcan set its owncontract,revertandpromisesfor the runs it starts.- A run that promised a kind or a thread and wrote nothing to it reads
stopped_shortwith the reasonpromised <kind>, wrote none, never done. - A failed item in a batch releases its slot, and a question the actor asks again replaces its older unanswered copy on your register.
When a hosted workspace sleeps
A hosted workspace sleeps when nobody is using it. Scheduled triggers run while the workspace is
awake. When a trigger falls due while the workspace sleeps, the missed run is dealt with the next
time the workspace wakes, for example when you open it. The trigger's catch-up setting (catchup)
decides what happens: skip the missed run, run only the most recent one, or run each one it missed.
Waking a sleeping hosted workspace at the moment its next trigger falls due is built but not switched on yet. Until it is, do not rely on a schedule firing on time while nobody is using the workspace.
On the free plan, a trigger whose schedule fires more often than hourly is refused when you turn it
on (SCHEDULE_FINER_THAN_PLAN).
Turn a trigger off
# Remove by name (writes a tombstone; re-declaring the name brings it back)
spl trigger remove daily-summary
# Or pause without removing (keeps the whole definition)
spl trigger pause daily-summaryPairs with
- Routing rules: record-matching triggers with actions
- CEL expressions: the expression language for record-matching trigger conditions
- Actors and adapters: how dispatched actors actually run
Routing Rules
Control how records are routed to actors — match patterns, set targets, and let the system learn.
CEL Expressions
Write rules, gates, and predicates using Syncropel's canonical expression language — one syntax for triggers, routing, preconditions, fold rules, health checks, the decision gate, permissions, and fan-out join predicates.