Docs

Automations v2 — Design Notes

Plan-phase note for the automations modernization — unlike the delivery-notes siblings, nothing here has shipped. The decisions live in ADR-0022: Automations run as Deno subprocesses and ADR-0023: The workflow engine is in-app; this note holds the research they rest on, the spike gate, and the roadmap. Both ADRs are works-in-progress while proposed — modify and iterate freely until implementation starts.

Status

Phase 1 shipped (2026-08-13); Phases 2–3 are still design only. The Deno runner (ADR-0022) is implemented and its gate passed — ADR-0022 is now Active. See the Phase 1 delivery note for the as-built record. The in-app engine (ADR-0023) remains proposed, gated on the Phase 2 spike below.

PhaseScopeGateState
ADRs + this notemanual read-through✅ done
1Deno runner + WS bridge + Runs panel + ported examplesall seeded examples passshipped (ADR-0022 Active)
2in-app engine spike (checklist below)every item passesnot started
3SDK + workflows, individual mode (Mode A), durable-execution v1see roadmapnot started

Cross-client execution is no longer a numbered phase — see “Graduation paths” at the end of the roadmap.

Current-state architecture (what v2 replaces)

The runner is ~230 lines of tranquil-automations/lib/tranquil-automations.js. A script’s text becomes the body of new AsyncFunction("tranquil", "require", "atom", "__dirname", "__filename", content), executed in the host renderer — full Node (require, fs, child_process via tranquil.exec), full atom, no sandbox, not killable, no history.

Browser control is puppeteer-core connected to http://localhost:9222 (the CDP port every window shares, enabled in src/main-process/start.js). getActiveTab() matches a tracked _lastWebView URL against CDP targets — a heuristic that goes stale, and that breaks outright once a script navigates a tab. openBackgroundTab() hand-builds off-screen <webview> elements because Electron’s CDP lacks Target.createTarget (browser.newPage() throws).

The tranquil.* surface: getActiveTab / getTab / getTabs / openTab / openBackgroundTab / closeTab (puppeteer Pages), writeFile / readFile / openFile (relative to the script dir), notify, clipboard, getProjectDir, exec, config (raw atom.config), paneControls.register, autoInject.register/unregister.

Triggers: cmd-shift-R runs the active editor’s selection or file; palette commands from registered files persisted in atom.config; auto-inject on webview did-stop-loading for URL-matched scripts.

Fragility list: one fixed :9222 port (single instance; contended with native DevTools; open to any local process), URL-match tab identity, _lastEditor/_lastWebView heuristics, scripts with exec+fs+atom and no consent step, no cancel, no run history, eval-frame stack traces.

Decision log

2026-08-06:

  1. Scope: design docs first. ADRs + this note; no implementation until after a read-through.
  2. Runner model: Deno subprocess + RPC bridge. Scripts in deno run under computed permission flags; app capabilities over Cap’n Web via a new WebSocket transport; browser control via Deno-native CDP.
  3. Script API: clean break. Best Deno-native API; port the seeded examples; no shims; the renderer runner dies.
  4. celld spike on a local MinIO bucket — individual-client first; shared bucket (R2/Tigris + Tailscale) is the promotion path. (Superseded by 8–11 below.)
  5. Multi-URL scripts are the common case — stable tab handles, first-class navigation, and capped background fan-out are design-center (ADR-0022, specific 6).
  6. Durable execution is the eventual bar — Restate-class. v1 ships the minimal core (journaled steps, retries/backoff, durable sleep, awakeables, terminal-vs-transient errors, run introspection); the SDK step surface keeps the engine swappable (ADR-0023, specific 4).
  7. The client hosts the engine. Clarified in the same exchange: workflow code never executes in a webview — webviews only execute the browser portion of capability steps. (The celld/hub-and-spoke topology half of this entry is superseded by 8–11; the client-hosts-the-engine kernel survives as ADR-0023’s Mode A, stronger — the engine is now in-process.)

2026-08-11 — the dependency-weight review (a full ADR re-read with fresh questions: substrate dependencies, V8-vs-runtime confusion, packaged-build install story):

  1. celld demoted pre-spike (ADR-0023 Option C). The v1 journal would be hand-built on a pre-1.0, patches-by-email substrate; two daemons per user (celld + MinIO — MinIO AGPL-3.0 with an archived upstream13); esbuild on PATH; Workers dialect for workflow code. Its unique upside — bucket-lease any-device takeover — is not a near-term bet. Re-evaluation triggers recorded in the ADR.
  2. Restate evaluated as the successor, and demoted the same day (ADR-0023 Option B — now the named graduation path). Verified: BUSL-1.1 whose Additional Use Grant forbids only a “Public Restate Platform Service” — bundling restate-server for users’ own workflows is permitted production use, converting to Apache-2.0 four years per release14; v1.7.3 ships ~40 MB compressed macOS/Linux server archives with sha256s and Homebrew formulas, no Windows artifacts15. Demoted anyway: even bundled it is a server binary plus a supervised endpoint-host process — two children — whose distributed machinery a local single-writer engine never exercises.
  3. All-Node (zero external runtime) considered and rejected (ADR-0023 Option E). Verified: Electron 30 bundles Node 20 (permission model experimental, fs-only); Node 22’s stable model has no network permission at all16; a boolean --allow-net exists only from around Node 2517 — so for the whole Electron horizon a Node child cannot scope network egress, and ADR-0022’s @permissions net=… contract would be policy, not enforcement. Deno stays, as the enforcement layer.
  4. Final architecture: the engine is in-app, and Deno ships with the app. Scheduler + SQLite journal in the app’s main process; every workflow run is a Deno child through the ADR-0022 runner (one dialect, one toolchain, shared step-primitives module); per-run source snapshots give user-edit replay safety. Packaged builds bundle a pinned Deno — overturning 0022’s “bundling rejected for now” — so there are zero daemons and zero user-visible installs. Cross-platform rules encoded up front: protocol-level cancel (Windows has no graceful SIGTERM), token-socket run channel (no fd passing, no UDS), loopback-only webhooks, engine data on a guaranteed-local disk.

Research summary

celld1 (denoland/celld2): self-hosted distributed Durable Objects + Workers; single binary; V8 executing Wrangler bundles; a cell = named single-threaded DO with private SQLite, HTTP, hibernatable inbound WS + outbound ws:/wss:, alarms, outbound fetch. Writes replicate to an S3-compatible bucket before ack (RPO = 0); nodes coordinate through bucket leases + CAS only; peer HTTP is HMAC-authed, intended for private overlays. Compat3: JS RPC (WorkerEntrypoint/RpcTarget, pipelining), service/DO bindings, assets, partial node:. No cron triggers, queues, KV, R2, or Cache API — not planned. Planned: D1, “Workflows (durable execution)“. Pre-1.0, Apache-2.0, patches by email. (Historical context — demoted per decision 8; retained because ADR-0023 Option C’s re-evaluation triggers reference it.)

Restate license and distribution (verified 2026-08-11): server under BUSL-1.1; the Additional Use Grant forbids only a “Public Restate Platform Service” (a managed service exposing Restate APIs to third parties), so bundling for users’ own workflows behind our SDK is permitted; each release converts to Apache-2.0 after four years14. Release v1.7.3 assets: restate-server for aarch64/x86_64-apple-darwin and -unknown-linux-musl at ~40 MB compressed, with .sha256 files and Homebrew formulas; no Windows artifacts15.

Node permission model (verified 2026-08-11): v22 (the Electron horizon — Electron 30 bundles Node 20, Electron ~35–37 bundle Node 22) has a stable permission model covering path-scoped fs read/write plus boolean child-process/worker/addons/WASI — and no network permission16. Current Node (≥ ~25) adds --allow-net as a coarse grant with no documented per-host scoping17. Deno’s per-resource model (--allow-net=host:port, --allow-env=NAME, --allow-run=cmd) has no Node equivalent5 — the fact that keeps Deno as the runner and killed the zero-external-runtime option.

capnweb: first-class WebSocket transport — newWebSocketRpcSession(url) client / newWebSocketRpcSession(ws, api) server — explicitly supports Deno; custom transports implement RpcTransport { send, receive, abort? }, matching tranquil-rpc’s existing encodingLevel: "string" contract4. Runtime behavior on Deno (via npm:capnweb) remains a Phase 1 verify.

Deno permission model5: every --allow-* scopes to resources (--allow-net=host:port, --allow-read=path, --allow-env=NAME, --allow-run=git); --no-prompt fails closed. Config resolution and import maps via deno.json + --config6. Deno is an MIT-licensed single static binary with official releases for macOS, Linux, and Windows18 — what makes the bundled-Deno decision (log 11) clean.

Existing Deno beachheads: tranquil-test-suite (Deno-only; hand-rolled ~150-line CDP client in smoke/lib/cdp.ts against this exact Electron — the seed of ADR-0022’s owned client) and this website (SvelteKit on Deno). The Engine-era drafts already picked Deno + Hono (inbox draft ADR-0007).

Browser-control weigh-off: puppeteer-core7 (heavy; Node-compat risk on Deno; its Electron gaps are what the current code hacks around) vs astral8 (Deno-native, pre-1.0, launch-oriented — attach-to-existing-Electron unverified) vs owned CDP client (chosen; smallest honest surface, grown from proven code).

Durable-execution survey — grounded in in-house experience: inbox draft ADR-0002 already ran Restate in the healthcare-era backend (journal + replay9, keyed-workflow dedup, console introspection, the service/object/workflow construct split; live Restate Cloud environments per deployment management) and already compared SQS + Lambda, Step Functions, XState (~135 vs ~70 lines on the benchmark workflow), and Vercel Workflow DevKit — those comparisons are cited, not redone. Cloudflare Workflows10 gives the step.do / step.sleep / step.waitForEvent shape. The v1 step surface (ADR-0023, specific 4) is deliberately the common subset of Restate’s ctx.run/ctx.sleep/awakeables11 and Cloudflare’s steps, so both engines remain retarget candidates behind the unchanged SDK.

Engine spike checklist (the Phase 2 gate)

Each item has a pass criterion; any failure is a no-go and re-opens ADR-0023 toward its Restate fallback (Option B).

  1. Journal write-ordering / crash-replaykill -9 the app immediately after a step result is acked, and again mid-step; relaunch. Pass: the run resumes, completed steps are not re-executed, the interrupted step re-executes.
  2. Child death — kill the run child mid-step. Pass: the engine’s watchdog marks the attempt failed, retry/backoff fires per the step’s options.
  3. Schedule catch-up — a schedule comes due while the app is closed. Pass: it fires via the catch-up scan on next launch (Mode A’s everyday case), and again after machine sleep via the powerMonitor resume hook12.
  4. Awakeable park/resume — a capability step with no eligible window parks durably (run suspended, not failed) and resumes when a window opens; device/session affinity respected.
  5. Edit safety — edit the workflow file while a run is mid-flight. Pass: replay executes the per-run snapshot, unaffected; the next run picks up the edit.
  6. SQLite under Electron main — better-sqlite3 builds via electron-rebuild; WAL behaves through kill tests; confirm the engine data dir is local-disk.
  7. Webhook listener — loopback HTTP route /hooks/<wf> with a stable local URL; port strategy decided.
  8. Runner reuse — a run child spawns through the ADR-0022 bridge with runner-audience capabilities; the step-primitives module is shared verbatim with local scripts (ephemeral journal in scripts, durable in runs).
  9. Scan under load — many concurrent runs with pending timers: measure granularity, drift, and UI-thread impact (the engine must never jank the renderer).
  10. Semantics end-to-endTerminalError fails immediately; transient errors retry with backoff; per-attempt timeout enforced; tranquil runs describe shows current step, attempt, last error, next retry.

celld spike checklist (demoted — retained for future re-evaluation)

The Phase 2 gate for the superseded celld substrate, kept intact should Option C’s re-evaluation triggers fire (native “Workflows” ships; a MinIO-free bucket backend exists; cross-device becomes a near-term bet).

  1. Deploy pipeline — bundle and celld deploy a hello Worker + two DO classes to local MinIO via a deno task (esbuild pipeline). Pass: routes serve, cells instantiate.
  2. Persistence / RPO = 0kill -9 celld immediately after an acked write; restart. Pass: state present.
  3. Alarm durability + catch-up — set an alarm; stop the daemon; restart before and after the due time. Pass: documented fire/catch-up semantics, including alarms that came due while the daemon was down.
  4. ClientHub WebSocket — Deno client ↔ hub cell: idle-hibernation behavior, server-initiated request → client → response roundtrip and its latency. Pass: correlated frames survive hibernation.
  5. DO-binding JS RPC — calls + promise pipelining between cells behave as documented.
  6. Authoring DX — Workers-typed TS checked by Deno tooling; source maps in runtime errors.
  7. Hono in a cell — the inbox draft ADR-0007 stack (Hono + zod-openapi) runs inside a celld Worker.
  8. Failure modes — bucket down (write behavior?), idle daemon memory/CPU, bucket growth per write.
  9. Hub protocol probe — capnweb inside a DO vs plain correlated frames.
  10. Crash-resume — kill the daemon mid-run between journaled steps; on restart the alarm re-invokes the run, it replays from the journal, and completed steps are not re-executed.
  11. Alarm multiplexing under load — granularity, drift, re-arm-in-handler reliability with many pending timers (DO alarms are one-per-object12a).
  12. Cross-node takeover, run locally — two celld nodes sharing one MinIO bucket; kill the owner mid-run. Pass: the second node acquires the lease, materializes SQLite from the bucket, pending alarms fire on the new owner, the run resumes without re-executing finished steps.

Open questions

  • npm:capnweb on Deno — runtime verification (Phase 1).
  • Per-instance CDP port strategy (:9222 is one fixed port; the token/ephemeral-port bridge is the later template).
  • Webhook listener port strategy (stable local URL vs ephemeral + registry).
  • Child↔engine token-socket step-protocol details (framing, heartbeat cadence, cancel grace).
  • better-sqlite3 vs waiting for Electron’s node:sqlite.
  • Snapshot + journal retention/GC for completed runs.
  • Parallel steps (v1 is sequential).
  • Mode B per-OS supervision recipes (LaunchAgent / systemd user service / Task Scheduler logon task) — deferred with Mode B.
  • macOS notarization: JIT entitlements for the bundled deno — verify at the first signed build.
  • Universal-vs-per-arch macOS packaging for the bundled binary.
  • Windows engine-data-dir locality (roaming-profile %USERPROFILE% vs WAL).
  • Editor TS tooling for script authors inside an Atom-lineage editor (deno lsp — the bundled binary can serve it).
  • Windows support generally (Phase 1 targets macOS).
  • A declarative (non-resident) paneControls registration form.
  • Background-tab session-partition inheritance (authenticated multi-URL flows depend on it).
  • Background-tab concurrency cap default (~4 is a guess; measure).
  • How the SDK expresses capability-request affinity (per-step option? inferred from the capability used?).
  • Deno binary distributionanswered (log 11): bundled — config override → bundled binary → PATH (dev only); DENO_DIR=~/.tranquil/deno-cache isolation.

Parked with the demoted celld option: MinIO licensing/alternatives13, non-owner request proxying (webhook-via-any-node), hub-protocol capnweb-vs-frames probe.

Phased roadmap and gates

  • Phase 1 — Deno runner (ADR-0022): subprocess runner + WS bridge + audience tagging + Runs panel + ported seeded examples + the new multi-URL fan-out example. Gate: all seeded examples (including multi-URL) pass on macOS; cancel/kill works; zero regression in the webview RPC suites. Passing flips ADR-0022 → Active.
  • Phase 2 — engine spike: the checklist above (journal, replay, catch-up, parking, snapshots, SQLite-under-Electron). Gate: every item passes → ADR-0023 flips Active. Any failure → re-open toward the Restate fallback (Option B — the proven incumbent), behind the unchanged SDK surface.
  • Phase 3 — SDK + workflows, individual mode, durable-execution v1 (steps, retries/backoff, durable sleep, awakeables, crash-replay) with the engine in-app under Mode A. Gate: webhook + schedule + manual all fire while the app is open; a schedule due while everything was closed fires via catch-up on next launch; one browser-callback workflow end-to-end (including parked-then-resumed); a workflow survives an app kill mid-run and completes without re-executing finished steps; editing a workflow mid-run does not disturb the in-flight snapshot.
  • Graduation paths (not a phase): if cross-device execution or scale becomes a near-term bet, retarget the unchanged step surface — Restate (ADR-0023 Option B: bundled local server, or Restate Cloud for shared mode) or a re-evaluated celld (Option C triggers). Nothing in workflow authoring may depend on the v1 journal, precisely so these stay cheap.

Docs housekeeping

  • Nav entries in src/lib/data/docs.ts for ADR-0022, ADR-0023 (retitled/re-slugged 0023-in-app-workflow-engine on 2026-08-11), and this note.
  • Automations v2 — Security Review (added 2026-08-11, Security nav group) — the design-time assessment against the living threat model; both ADRs carry its decision-level deltas (--allow-import, CDP-escape honesty, consent wording, webhook auth, unattended-consent rule), and the Security Considerations tracker gained a runner-principal section.
  • ADR-0015 carries an “Amended by ADR-0022” line — a third RPC principal (the local Deno runner, token-authenticated) now exists; host↔main stays classic IPC. ADR-0023 needs no further amendment: the engine speaks classic IPC to renderers, and run children are the ADR-0022 runner principal.
  • Inbox draft ADR-0007 (Hono) no longer carries forward into ADR-0023 — there is no Worker to host it; the API-surface question returns if a server component ever exists.

  1. MinIO — AGPL-3.0; upstream repository archived. https://github.com/minio/minio
  2. Restate LICENSE — BUSL-1.1; Additional Use Grant forbids only a “Public Restate Platform Service”; Change License Apache-2.0 after four years per release. https://github.com/restatedev/restate/blob/main/LICENSE
  3. Restate releases — v1.7.3 platform assets (macOS arm64/x64, Linux musl; no Windows), sha256s, Homebrew formulas. https://github.com/restatedev/restate/releases
  4. Node.js v22 permission model — stable; fs/child-process/worker/addons/WASI; no network permission. https://nodejs.org/docs/latest-v22.x/api/permissions.html
  5. Node.js current permission model — coarse --allow-net in recent Node; no per-host scoping documented. https://nodejs.org/api/permissions.html
  6. celld documentation. https://celld.dev/docs/
  7. denoland/celld — “self-hosted, distributed Durable Objects”; announcement: https://x.com/rough__sea/status/2085001943693549887. https://github.com/denoland/celld
  8. celld Cloudflare compatibility. https://celld.dev/docs/cloudflare-compat/
  9. Deno security and permissions. https://docs.deno.com/runtime/fundamentals/security/
  10. Cap’n Web — WebSocket transport, RpcTransport interface, Deno support. https://github.com/cloudflare/capnweb
  11. Deno configuration (deno.json, import maps, tasks, --config). https://docs.deno.com/runtime/fundamentals/configuration/
  12. Deno — MIT-licensed single static binary; official macOS/Linux/Windows releases. https://github.com/denoland/deno
  13. Puppeteer Page API. https://pptr.dev/api/puppeteer.page
  14. Astral — puppeteer/playwright-like browser automation for Deno. https://jsr.io/@astral/astral
  15. Restate durable execution — journal + replay. https://docs.restate.dev/concepts/durable_execution
  16. Cloudflare Workflows — step.do/step.sleep/step.waitForEvent. https://developers.cloudflare.com/workflows/
  17. Restate SDK actions — ctx.run, durable timers, awakeables. https://docs.restate.dev/foundations/actions
  18. Electron powerMonitorsuspend/resume on macOS, Windows, and Linux. https://www.electronjs.org/docs/latest/api/power-monitor
  19. Durable Object alarms — one per object, at-least-once execution. https://developers.cloudflare.com/durable-objects/api/alarms/