Rules are Springtale's core automation unit. A rule says: "when THIS happens, if THESE conditions are true, do THAT." Rules are authored in TOML, stored in SQLite, and evaluated by the rule engine with zero AI required.
┌──────────────────────────────────────────────────────────────┐
│ Rule │
│ │
│ id: UUID (auto-generated) │
│ name: "stream-announce" │
│ status: enabled | disabled | draft │
│ version: monotonic u64 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ Trigger │ │ Conditions │ │ Actions │ │
│ │ (exactly 1) │ │ (0 or more) │ │ (1 or more) │ │
│ │ │ │ all must │ │ run in sequence │ │
│ │ "when this │ │ pass (AND) │ │ or chain │ │
│ │ happens" │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └───────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Fig. 1. Rule structure. One trigger, zero or more conditions, one or more actions.
A trigger defines what event starts the rule. One trigger per rule.
TABLE I. TRIGGER TYPES
| Type | What fires it | Key fields |
|---|---|---|
Cron |
Timer on a cron schedule | expression: cron string (e.g., "0 */6 * * *") |
FileWatch |
Filesystem change detected | path: directory to watch, event: "create", "modify", "delete", or "any" |
Webhook |
HTTP POST to webhook endpoint | path: URL path (e.g., "/deploy") |
ConnectorEvent |
A connector emits an event | connector: connector name, event: trigger name |
SystemEvent |
Internal system event | event: event name |
Example triggers in TOML:
# Fire when a Kick stream goes live
[trigger]
type = "ConnectorEvent"
connector = "connector-kick"
event = "stream_live"
# Fire every 6 hours
[trigger]
type = "Cron"
expression = "0 */6 * * *"
# Fire when a file is created in /data/inbox
[trigger]
type = "FileWatch"
path = "/data/inbox"
event = "create"Conditions filter when a rule fires. All conditions must pass (AND logic at the top level). If a rule has no conditions, it fires on every trigger match.
TABLE II. CONDITION TYPES
| Type | What it checks | Example |
|---|---|---|
FieldEquals |
Exact value match on a payload field | field = "trigger.event", value = "stream_live" |
Contains |
Substring match | field = "trigger.title", value = "minecraft" |
Regex |
Regex pattern match | field = "trigger.text", pattern = "^!\\w+" |
TimeInRange |
Current time within range | start = "09:00", end = "17:00" |
DayOfWeek |
Current day matches | days = [1, 2, 3, 4, 5] (Mon-Fri) |
And |
All sub-conditions pass | conditions = [...] |
Or |
Any sub-condition passes | conditions = [...] |
Not |
Sub-condition fails | condition = { ... } |
Conditions support dotted field paths into the trigger payload. For example, trigger.sender.name resolves through nested JSON objects. Array indexing works too: trigger.commits.0.message.
Constraints: max nesting depth of 8 levels. Regex patterns limited to 1MB (prevents ReDoS).
# Only fire during business hours on weekdays
[[conditions]]
type = "TimeInRange"
start = "09:00"
end = "17:00"
[[conditions]]
type = "DayOfWeek"
days = [1, 2, 3, 4, 5]Actions are what the rule does when it fires. They run in sequence.
TABLE III. ACTION TYPES
| Type | What it does | Key fields |
|---|---|---|
RunConnector |
Execute a connector action | connector, action, params (key-value map) |
SendMessage |
Emit a text message | text |
WriteFile |
Write content to a file | destination, content, delete_source (bool) |
RunShell |
Execute a shell command | command |
Notify |
Send a notification | title, body |
Chain |
Run nested actions in sequence | steps (list of actions, max depth: 4) |
Transform |
Apply a data transformation | operation, params |
Delay |
Wait before next action | seconds |
AiComplete |
Optional AI call through the configured adapter | prompt, adapter (optional) |
Action fields support ${trigger.field} template syntax. Variables resolve against the trigger event payload at dispatch time.
[[actions]]
type = "RunConnector"
connector = "connector-bluesky"
action = "create_post"
[actions.params]
text = "${trigger.username} is live on Kick: ${trigger.title}"Available variables depend on the trigger. For example, a connector-kick stream_live trigger provides ${trigger.broadcaster.username}, ${trigger.title}, ${trigger.started_at}.
Under the hood, actions flow through a pipeline of stages. Each stage reads from and writes to a PipelineContext — a data bag carrying input, output, errors, and metadata.
TriggerEvent
│
v
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Stage 1 │────>│ Stage 2 │────>│ Stage 3 │────>│ Stage N │
│ │ │ │ │ │ │ │
│ reads │ │ reads │ │ reads │ │ reads │
│ ctx.input│ │ ctx.output│ │ ctx.output│ │ ctx.output│
│ writes │ │ writes │ │ writes │ │ writes │
│ ctx.output│ │ ctx.output│ │ ctx.output│ │ ctx.output│
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │
│ PipelineContext flows through │
└───────────────────────────────────────────────────┘
Fig. 2. Pipeline stage composition. Stages execute left-to-right. First failure short-circuits the pipeline.
The PipelineContext carries:
trace_id— UUID for tracing a request through the systeminput— original trigger payloadoutput— current data (modified by each stage)errors— collected error messagesretry_count— how many times this pipeline has retriedchain_depth— current nesting depth (max 4)
When a Kick stream goes live, announce it on Bluesky. No AI needed.
[rule]
name = "stream-announce"
[trigger]
type = "ConnectorEvent"
connector = "connector-kick"
event = "stream_live"
[[actions]]
type = "RunConnector"
connector = "connector-bluesky"
action = "create_post"
[actions.params]
text = "${trigger.broadcaster.username} is live: ${trigger.title}"When a file appears in /data/inbox, move it to /data/archive.
[rule]
name = "auto-archive"
[trigger]
type = "FileWatch"
path = "/data/inbox"
event = "create"
[[actions]]
type = "WriteFile"
destination = "/data/archive/${trigger.filename}"
content = ""
delete_source = trueRun a backup script every day at 3 AM.
[rule]
name = "daily-backup"
[trigger]
type = "Cron"
expression = "0 3 * * *"
[[actions]]
type = "RunShell"
command = "backup-script"When a PR is opened, create a Bluesky post AND post a GitHub comment.
[rule]
name = "pr-announce"
[trigger]
type = "ConnectorEvent"
connector = "connector-github"
event = "pull_request_opened"
[[conditions]]
type = "FieldEquals"
field = "trigger.repository"
value = "ScopeCreep-zip/Springtale"
[[actions]]
type = "Chain"
[[actions.steps]]
type = "RunConnector"
connector = "connector-bluesky"
action = "create_post"
[actions.steps.params]
text = "New PR on Springtale: ${trigger.title} by ${trigger.author}"
[[actions.steps]]
type = "RunConnector"
connector = "connector-github"
action = "post_comment"
[actions.steps.params]
owner = "ScopeCreep-zip"
repo = "Springtale"
issue_number = "${trigger.number}"
body = "Thanks for the PR! Reviewing shortly."- [1] CLI rule commands: reference/cli.md
- [2] API rule endpoints: reference/api.md
- [3] Connector triggers and actions: reference/connectors/
- [4] Full rule engine spec:
docs/current-arch/ARCHITECTURE.md§6.1