feat(database,hosted): automations research, and the automation and automation_run tables with their store (automations 1/7)
Automations 1/7: the market research for scheduled and triggered agents, and the first backend layer for them. Nothing reads these tables yet. The schedule engine (2/7), run execution (3/7), the API (4/7) and the triggers (5ā6/7) come in later PRs in this stack.
Research
docs/proposals/automations-research.md covers how other products do scheduled and event-triggered agents, with a URL for every claim:
- Coding-agent products: Cursor, Devin, Codex, Copilot and GitHub Actions, Claude Code Routines, Conductor routines, Jules, Factory, Warp, Amp, Replit and Sweep.
- Automation platforms: Linear Loops, Slack Workflow Builder, Zapier, Make, n8n, Lindy and Relay.
- Infrastructure: Vercel Cron and Connect triggers, and the GitHub, Stripe and Standard Webhooks delivery semantics.
It ends with the patterns that matter, the pitfalls, and a recommended v1 for Luke. Two corrections to the brief:
--trigger-pathis a flag ofvercel connect attach, not ofcreate.- Relay.app shut down in September 2026.
Wire vocabulary
packages/hosted/src/automation-wire.ts declares each shape once, as Effect schemas:
- Trigger:
schedule: cron plus an IANA zone.github: an event, plus optional label and branch filters.webhook.
- Action: a coding agent, given a prompt and an optional model choice.
- Guards: max concurrent runs (1ā5) and a daily cap (1ā50), defaulting to 1 and 10.
- Firing: schedule, github, webhook or manual.
- Run status:
queued,started,skippedorfailed. - Run reason: a fixed word, so nothing a provider says reaches the run history.
Tables
packages/database/src/schemas/automation.ts and the generated migration 20261014130146_automation add:
automation:- Belongs to the account and one of its plans.
- Trigger, action and guards are stored as the wire's JSON.
trigger_kindandnext_run_atare real columns because statements filter on them.- The sweep's partial index covers
next_run_at is not null. webhook_secret_hashholds only a SHA-256, never the secret.enabledandpaused_reasonrecord whether it is paused and why.
automation_run:- One row per firing.
- A unique index on
(automation_id, dedupe_key)makes a firing claimed twice record one run. coding_agent_idisset null, so a deleted agent leaves its run in the history.
Both tables are new and the change is purely additive, so v0.12.0 is unaffected.
Store
packages/database/src/models/automation-store.ts is effects over the ambient SqlClient:
createAutomationwrites only on a plan the account reaches (reachablePlan). Otherwise it answers none.- Every other read and write is keyed by the account.
recordAutomationRunuseson conflict do nothingon the dedupe key, and answers none for a repeat.settleAutomationRunmoves a run fromqueuedto one outcome, once.listAutomationRunsandautomationRunsSinceread the history; the guards (3/7) count recent runs with the second.
Tests
packages/database/src/models/__tests__/automation-store.test.ts runs on PGlite:
- An account outside the plan's workspace makes nothing on the plan, and cannot read, change, delete or list another account's automation or its runs.
- A change leaves the fields it doesn't name as they were.
- The same firing recorded twice makes one run.
- A run settles once.
- The "since" read counts only runs at or after the given instant.
storage-schema.test.ts now lists the two new tables.
pnpm check:affected passes. CI's Postgres job runs the store tests on a real Postgres.
No UI changes in this PR, so there is no macOS evidence and verify.sh was not run.
š¤ Generated with Claude Code
Summary by cubic
The first part of the automations work: the market research behind scheduled and triggered agents, and the backend layer that stores them. Nothing reads the new tables yet; the schedule engine (2/7), run execution (3/7), the API (4/7), and the triggers (5ā6/7) follow in this stack.
Research
docs/proposals/automations-research.mdsurveys how coding-agent products (Cursor, Devin, Codex, Copilot, Claude Code Routines, Conductor, Jules, Factory, Warp, Amp, Replit) and automation platforms (Linear Loops, Slack, Zapier, Make, n8n, Lindy, Relay) handle scheduled and event-triggered agents, with a URL for every claim.- It concludes with the patterns that matter, the pitfalls, and a recommended v1 for Luke.
Database and store
packages/hosted/src/automation-wire.tsdeclares each shape once as Effect schemas: schedule/GitHub/webhook triggers, the coding-agent action, concurrency and daily-cap guards, and run statuses and reasons.- The new
automationandautomation_runtables are purely additive. Trigger, action, and guards are stored as JSON;trigger_kindandnext_run_atare real columns because statements filter on them. Webhook secrets are kept as SHA-256 only. automation_store.tsscopes every read and write to the account's own automations and gates each on the account still reaching the automation's plan;createAutomationrefuses plans the account doesn't reach. Changing a trigger clears the columns only its kind uses:next_run_atoutside a schedule,webhook_secret_hashoutside a webhook.- A unique index on
(automation_id, dedupe_key)makes one run per firing; a run settles once, from queued to one outcome.