SSyncropel Docs

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 serve instance
  • 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 600

Cron 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-triggers

Look for your new entry. Confirm it's enabled: true and the schedule matches. To see past firings:

spl config trigger-history --limit 10

Each 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 good

remove 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 300

Trust 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.3

Fleet 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 180

Cron expression cheatsheet

SpecFires
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 * * MON10:00am every Monday
0 0 1 * *Midnight on the 1st of every month
30 8 * * MON-FRI8: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.writes names the threads its runs may write, and work_loop.write_kinds the 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.revert says 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.contract and work_loop.promises state what a run commits to and produces. A trigger's dispatch can set its own contract, revert and promises for the runs it starts.
  • A run that promised a kind or a thread and wrote nothing to it reads stopped_short with the reason promised <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-summary

Pairs with

On this page