Automations & buttons
Rules that run actions when records change or on a schedule. Open them from a database’s
⋯ menu → Buttons & automations.
The one thing to know first
Section titled “The one thing to know first”Buttons and rules run the same actions. There is a single action set in StoryOS, and every action in it is available to a button and to a scheduled or triggered rule. If you have seen an action offered on a button, a rule can do it too — including sending email and calling any HTTP API, unattended, with nobody clicking anything.
The full action list
Section titled “The full action list”Eleven actions, all available to buttons and rules alike.
| Action | What it does |
|---|---|
| Set fields on this record | Writes values onto the record that triggered the rule. |
| Create a record | Creates one record in any database, optionally linked back through a relation — or matches an existing one on a unique key instead of duplicating it. |
| Create many records | Creates 0–200 records from one template. The count can be a number or a {Field} token read at run time; {index} (1-based) differentiates them — “Day {index}”. |
| Add a comment | Posts a comment on the record. |
| Notify a person | An in-app notification to a person field’s value, @me, or a specific workspace member. |
| Update linked records | Sets fields on every record linked through a chosen relation. |
What a “field to set” can be. For both of the actions above the picker offers the record’s title (a plain text box) and relation fields (a record search, to choose which records to link), as well as ordinary fields. Lookups, rollups, buttons, rich text and the automatic fields (created at, updated at, created by) are not offered, because they can’t be set by hand. | Send a Slack message | Posts to a channel, or the workspace’s default channel. | | Send a webhook | POSTs to a URL you name, with the standard record payload or your own body. | | Send an email | A templated 1:1 email through a Resend or SMTP connection. See below. | | Call an API | Any method, any URL, optional auth, and it can read the response back onto fields. See below. | | Run an agent | Launches an agent from the Agents database against the triggering record. |
Every action supports the @me / @today / @now tokens and {Field Name} interpolation.
A rule carries between 1 and 10 actions.
Three different things are called “webhook” here. Send a webhook (above) and webhook received (below) are per-rule, one action or trigger in a chain of many. A webhook subscription — Settings → Webhooks, its own signing secret and delivery log — is a separate, workspace-wide feature with no rule involved. See the webhooks guide.
Computing a number
Section titled “Computing a number”Setting a number field, a token expression is evaluated, not substituted. So
{Remaining} - 1 writes 4 when Remaining is 5 — it does not write the text "4 - 1" and then
fail validation.
| Goal | Set |
|---|---|
| Count down | Remaining = {Remaining} - 1 |
| Count up | Times Contacted = {Times Contacted} + 1 |
| Derive a total | Total = {Unit Price} * {Quantity} |
- Same expression language as formula fields — same parser, same operators, same rules about what an empty value does.
- Only number targets compute. A text field still does literal substitution, so
{Name} - 1on a text field writes exactly what it always wrote.
Anatomy of a rule
Section titled “Anatomy of a rule”When (trigger) → Only if (condition) → Then (actions)
- Triggers: record created · record changes (optionally scoped to one field — “when State changes”) · record linked or unlinked through a relation · schedule (hourly / daily / weekly at HH:mm, server time) · webhook received (an inbound URL the outside world POSTs to).
- Condition: any filter the views support; for scheduled rules the condition is the selection — the rule runs over every matching record, or the top/bottom N of them when the rule also carries a sort (below).
- Actions: the eleven above. Each action can also carry its own condition, checked against the record just before that action runs — so a rule can validate first and only then fire an email or an API call. A failed per-action condition skips that one action and the rest still run.
Notifying a specific member
Section titled “Notifying a specific member”Notify a person can name one workspace member directly — useful when the record has no person
field, such as a public careers-form submission that should alert a particular hiring lead. In the
rule editor the recipient picker lists the workspace’s members; over the API and MCP the target is
@member:<user id> (ids come from list_members).
- Checked when you save. The id must belong to an active member of this workspace; a non-member, or someone who has since been removed, is rejected with a clear message rather than silently accepted.
- A webhook-triggered rule can still only notify
@me— it has no triggering record to resolve a person from, and the specific-member target doesn’t change that. - Rules already using
@meor a person field work exactly as before.
Seeing a rule as a diagram
Section titled “Seeing a rule as a diagram”Every rule row has a Diagram toggle, next to Runs and Delete — it expands the exact anatomy above (trigger → condition → actions) as a read-only flow diagram, in order, so you can see what a rule does without reading a form top to bottom.
- Read-only, by design. Nothing in the diagram drags, connects, or deletes — it’s a projection of the rule’s existing configuration, not a second place to edit it. Edit in form underneath jumps to the same rule editor every rule already has.
- A per-action condition renders as a branch on that action — a dashed “Only if …” callout under the step it gates — the same way the rule-level condition renders above the first action.
- A “Create records” action shows a fan-out icon, since it can produce more than one record per run, unlike every other action.
- A trigger or action type the diagram doesn’t recognize still renders honestly — its raw type name, with a small “(diagram can’t draw this one yet)” note — rather than failing to open. A rule the diagram can’t fully draw is still a rule you can read and still one you can edit.
- A
record_linkedtrigger names its relation field and, when set, whether it fired on link, unlink, or either.
Editing a rule’s actions on a canvas
Section titled “Editing a rule’s actions on a canvas”When you edit a rule, its actions can be shown as a List (each action’s full settings, as always) or as a Canvas — the same trigger → condition → actions picture as the diagram above, but editable. A List / Canvas switch sits above the actions.
- Reorder by dragging an action by its handle; the order you leave them in is the order they run.
- Add action at the bottom opens the same grouped list of action types the List view offers, with the same starting defaults.
- Remove is on each action (the trash icon).
- Edit settings on an action switches you back to the List, focused on that action — the canvas moves and adds steps; the List is still where an action’s fields are filled in.
- It’s one rule, not two. Both views edit the same list of actions, so switching never loses anything and a rule you open and save without changes is identical whichever view you last looked at.
- Deliberately simple. A rule is a straight line of actions with, at most, one optional condition on each — there are no loops, no parallel branches and no rejoining paths.
Creating or upserting a record
Section titled “Creating or upserting a record”A Create a record action can be configured to match an existing record on a unique
field instead of always inserting —
the same match-or-create logic the upsert_record MCP tool exposes, so a rule that runs on a
schedule against a CRM export, say, doesn’t create a fresh duplicate every time it sees an email
address it’s already recorded. A match either updates the existing record with the rule’s
values, or (configured to skip) leaves it untouched — useful when the rule should only fill in
records that don’t exist yet, never overwrite ones that do.
This is API/MCP-only today — there’s no toggle for it in the rule editor’s own UI yet, so
configuring it means setting upsert: { key_field_id, on_match } on the action directly through
create_automation/update_automation.
Acting on a leaderboard, not every match
Section titled “Acting on a leaderboard, not every match”A scheduled rule can carry a sort (up to 3 keys, the same sort spec a view uses) and a limit (capped at 200), turning “every record where State = Published” into “the top 5 of them, by engagement.” A sort with no limit still orders the selection; a limit with no sort just caps it — neither implies the other. The rule editor’s Select row carries both: the same sort picker views use, and a record-count field that states the 200 ceiling right next to it — “records (up to 200) — leave blank for no cap” — so the limit you’re not allowed to exceed is visible before you save, not discovered as a rejected save.
Every other trigger rejects both fields outright. “Top five” means nothing for a rule firing on
one just-created or just-changed record, so a non-scheduled rule’s editor shows a short explanation
in place of the sort/limit controls instead of a silently absent one — and sort/limit are
refused on save regardless, whether they arrive alongside the trigger or get patched onto a rule
whose trigger already isn’t schedule.
- Ties break on record id, ascending — the same deterministic tail an ordinary paginated query already uses. A record with no value on the sort field sorts last, regardless of ascending or descending.
- A renamed, retyped, or deleted sort field fails loudly. That tick’s run is logged as an error naming the field — never a silent reorder and never a silent no-op — and nothing runs for that rule until the sort is fixed.
- A sort without an explicit limit still caps at 500 (the same ceiling an unsorted scheduled rule already had) — 200 is the ceiling on a limit you set, not the default when you leave it unset.
- Each selected record’s rank is on its run row (
selection_rank, viaget_runsor the workspace-wide runs endpoint) — “why did it pick these five” reads directly off the log, not a re-derivation from the rule’s current sort.
Sending email
Section titled “Sending email”An email action needs a connection — StoryOS never sends as an address you have not proved
you own. Set one up at Settings → Connections (Resend or SMTP), including a from-address;
for Resend, on a domain Resend has verified for that key. The connection’s own from-address is
what the mail sends as. You cannot type a from on the action.
Fields: to and cc (comma-separated, templated), reply_to, subject, and a markdown body.
All of them interpolate {Field Name} and {payload.…} tokens.
Approval is on by default, and the default is decided at run time. When you have not set the approval flag either way, StoryOS renders the recipients and checks them: if every resolved address belongs to a member of this workspace, the email sends immediately; if even one address is external, the send waits for a human approval. Setting the flag explicitly always wins — but only a workspace admin can save an email action with approval turned off. Any member can turn it on.
What goes wrong, and what you will see:
- Connection missing or deleted → the rule refuses to save: “send_email references an unknown connection”.
- Connection is not Resend or SMTP → refused, naming the provider you actually picked.
- Connection has no from-address yet → refused, telling you to reconnect it with one.
- Non-admin turning approval off → “Only a workspace admin can turn off approval for a send_email action.”
Calling an API
Section titled “Calling an API”Method (GET/POST/PUT/PATCH/DELETE), a templated URL, optional headers, optional body.
The URL is a template, so https://api.example.com/orders/{payload.id} is legal — which means
the host is only checked when the request is actually sent, not when you save the rule.
Auth belongs on a connection, not in the action. Create an http connection (bearer token,
basic auth, or a set of headers) and reference it. Its credentials are merged into the request
at send time only; they are never written into the stored rule and never appear in a run log.
Capture reads values out of a 2xx JSON response and writes them onto the record. Each capture
row is a dot/array path (a leading $. is stripped, so jq-style paths work) and a target field.
Up to 10 rows.
What goes wrong, and what you will see:
- Any status ≥ 400 fails the action, with the status and a body snippet in the run log. 5xx and 429 are retried with backoff; 4xx is not.
- A blocked host (StoryOS refuses to call private/internal addresses) fails without retrying — it is a refusal, not a blip.
- The response body is capped at 1 MB, and the logged result or error at 8 KB.
- If capture cannot parse the response as JSON, the request still counts as successful and the
run records a separate
capture_error— the call happened, the fields did not update.
What a webhook-triggered rule cannot do
Section titled “What a webhook-triggered rule cannot do”A rule triggered by webhook received has no triggering record, so it can only run actions that
do not need one: create a record, create many records, send a Slack message, send a webhook,
and notify a person (@me only). Saving any other action on such a rule is refused, and the error
lists what is allowed. Email and API calls are not available on a webhook-triggered rule — have
the webhook create a record, and let a record-created rule do the outbound work.
Triggering a rule from another tool
Section titled “Triggering a rule from another tool”Pick A webhook is received as the trigger, save the rule once, and it gets its own URL and secret. Paste the URL into whatever should trigger it — Typeform’s Connect → Webhooks, or a Zapier/Make “Webhooks” action. Both the URL and the secret have a Copy button, and Regenerate mints a new pair (the old one stops working immediately).
{payload.…} tokens read the delivered body in any action field: {payload.email}, or
{payload.answers.0.value} for something nested. The panel shows the last received payload to
find the path you need.
Signing is optional: sign requests with the secret (X-StoryOS-Signature: sha256=… plus
X-StoryOS-Timestamp) and StoryOS verifies them. Unsigned requests are still accepted, which is
what lets a no-code tool call the endpoint with nothing but a URL.
Recipes
Section titled “Recipes”| Goal | Rule |
|---|---|
| Escalate urgent tickets | When State changes · only if State is Urgent · add comment “⚠️ {Title} needs eyes” |
| Auto-stamp start dates | When State changes · only if State is In Progress · set Started = @today |
| Close stale Done work | Every day at 03:00 · only if State is Done · set Archived ✓ |
| Kickoff checklist on new client | When a record is created (Clients) · create a record in Tasks “Kickoff {Title}” linked back |
| Email the client when work ships | When State changes · only if State is Delivered · send an email to {Client Email} |
| Enrich a new lead | When a record is created · call an API GET https://api.example.com/lookup/{Domain} · capture company.size onto Headcount |
| One task per sprint day | When a Sprint is created · create many records, count {Length In Days}, title “Day {index}” |
| Auto-assign triage | When a record is created · set Assignee = @me |
Semantics worth knowing
Section titled “Semantics worth knowing”- Attribution: runs act as the rule’s creator; activity shows their name on the changes.
- Loop guard: an automation’s own writes can trigger other rules at most one more hop (depth 2). Deeper cascades are skipped and logged — a rule can never ping-pong forever. One narrow exception lets a countdown finish; see below.
- External actions are queued, not inline. Email, API calls and agent runs are handed to a durable job runner rather than run during the record’s save, so a slow or flaky endpoint retries with backoff instead of stalling the write or firing twice on a retry.
- Auto-disable: 10 consecutive failures switch a rule off (banner in the panel); editing the actions or re-enabling resets the streak.
- Run log: every execution (ok / error / skipped / skipped_quota) with duration is kept for 90
days, queryable across the whole workspace at
/w/:ws/runs. - Dry run:
POST …/automations/:id/test { record_id }answers “would this run?” without writing. - CSV imports do not fire automations (mass-import safety).
- Scheduled rules process up to 500 matching records per tick and note truncation in the server log.
Building a rule over MCP
Section titled “Building a rule over MCP”list_automations, get_automation, create_automation, update_automation, delete_automation
let an agent author a complete rule end to end — trigger, condition, and all 1–10 actions — not
only trigger it or read its run history. All five are admin-scoped only: a read- or
write-scoped token never sees them, matching the same restriction the app’s own rule editor sits
behind. delete_automation requires an explicit confirm.
These tools call the same validation the in-app rule editor uses, so there’s no second, looser path to an inconsistent rule — an unresolvable field or connection reference comes back as a structured error, never a 500.
When a rule stops itself
Section titled “When a rule stops itself”A rule whose actions change a record can trigger a rule again. StoryOS bounds that, and the run log tells you which bound you hit.
The ordinary limit: depth 2
Section titled “The ordinary limit: depth 2”An automation’s own writes get one more hop and no further. Anything deeper is skipped and logged, so a pair of rules cannot ping-pong forever and a rule cannot re-trigger itself endlessly.
In the run log this reads:
depth 2 — loop guard (rule "Escalate", watching field …); a self-trigger continues past depth 2 only while its watched number strictly decreases
The message names the rule and the field it watches, because “something stopped” without saying what is the version of this that costs you an afternoon.
The exception: a countdown is allowed to finish
Section titled “The exception: a countdown is allowed to finish”Depth 2 would break the most ordinary counting rule there is. “When Remaining changes, set Remaining = {Remaining} - 1” over a list of twenty items is a rule re-triggering itself nineteen times — and it is going somewhere, not looping.
So a self-trigger is allowed to continue while its watched number is strictly decreasing. Four conditions, all required:
- The trigger is record changes — not created, not linked, not scheduled.
- It watches a specific field. A rule that fires on any change has no single number whose descent could mean anything, so it gets the flat limit.
- That field’s value is a number, before and after.
- The new value is strictly less than the old one.
Equal does not count. A chain writing the same value over and over has stopped making progress and is still firing, which is the thing the guard exists to stop. Nor does a value that goes up, or a change to a non-number field — those fall back to depth 2.
Each step is judged on its own, so a chain continues exactly as long as every step of it decreases.
The ceiling on the exception: 25
Section titled “The ceiling on the exception: 25”Even a converging chain stops at depth 25:
depth 25 — converging, but hit the absolute ceiling of 25
25 is a working number, not a measured one — enough for a countdown over a sprint’s worth of items, far below anything that would strain an instance. A job that needs hundreds of steps wants create many records or a scheduled rule over a filter, not a self-triggering countdown.