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.
| Phase | Scope | Gate | State |
|---|---|---|---|
| — | ADRs + this note | manual read-through | ✅ done |
| 1 | Deno runner + WS bridge + Runs panel + ported examples | all seeded examples pass | ✅ shipped (ADR-0022 Active) |
| 2 | in-app engine spike (checklist below) | every item passes | not started |
| 3 | SDK + workflows, individual mode (Mode A), durable-execution v1 | see roadmap | not 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:
- Scope: design docs first. ADRs + this note; no implementation until after a read-through.
- Runner model: Deno subprocess + RPC bridge. Scripts in
deno rununder computed permission flags; app capabilities over Cap’n Web via a new WebSocket transport; browser control via Deno-native CDP. - Script API: clean break. Best Deno-native API; port the seeded examples; no shims; the renderer runner dies.
- celld spike on a local MinIO bucket — individual-client first; shared bucket (R2/Tigris + Tailscale) is the promotion path. (Superseded by 8–11 below.)
- 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).
- 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).
- 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):
- 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.
- 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-serverfor 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. - 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-netexists 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. - 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).
- Journal write-ordering / crash-replay —
kill -9the 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. - 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.
- 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
powerMonitorresume hook12. - 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.
- 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.
- SQLite under Electron main — better-sqlite3 builds via electron-rebuild; WAL behaves through kill tests; confirm the engine data dir is local-disk.
- Webhook listener — loopback HTTP route
/hooks/<wf>with a stable local URL; port strategy decided. - 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).
- Scan under load — many concurrent runs with pending timers: measure granularity, drift, and UI-thread impact (the engine must never jank the renderer).
- Semantics end-to-end —
TerminalErrorfails immediately; transient errors retry with backoff; per-attempt timeout enforced;tranquil runs describeshows 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).
- Deploy pipeline — bundle and
celld deploya hello Worker + two DO classes to local MinIO via adeno task(esbuild pipeline). Pass: routes serve, cells instantiate. - Persistence / RPO = 0 —
kill -9celld immediately after an acked write; restart. Pass: state present. - 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.
- ClientHub WebSocket — Deno client ↔ hub cell: idle-hibernation behavior, server-initiated request → client → response roundtrip and its latency. Pass: correlated frames survive hibernation.
- DO-binding JS RPC — calls + promise pipelining between cells behave as documented.
- Authoring DX — Workers-typed TS checked by Deno tooling; source maps in runtime errors.
- Hono in a cell — the inbox draft ADR-0007 stack (Hono + zod-openapi) runs inside a celld Worker.
- Failure modes — bucket down (write behavior?), idle daemon memory/CPU, bucket growth per write.
- Hub protocol probe — capnweb inside a DO vs plain correlated frames.
- 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.
- Alarm multiplexing under load — granularity, drift, re-arm-in-handler reliability with many pending timers (DO alarms are one-per-object12a).
- 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:capnwebon Deno — runtime verification (Phase 1).- Per-instance CDP port strategy (
:9222is 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)
paneControlsregistration 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 distribution— answered (log 11): bundled — config override → bundled binary → PATH (dev only);DENO_DIR=~/.tranquil/deno-cacheisolation.
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.tsfor ADR-0022, ADR-0023 (retitled/re-slugged0023-in-app-workflow-engineon 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.
- MinIO — AGPL-3.0; upstream repository archived. https://github.com/minio/minio↩
- 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↩
- Restate releases — v1.7.3 platform assets (macOS arm64/x64, Linux musl; no Windows), sha256s, Homebrew formulas. https://github.com/restatedev/restate/releases↩
- 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↩
- Node.js current permission model — coarse
--allow-netin recent Node; no per-host scoping documented. https://nodejs.org/api/permissions.html↩ - celld documentation. https://celld.dev/docs/↩
- denoland/celld — “self-hosted, distributed Durable Objects”; announcement: https://x.com/rough__sea/status/2085001943693549887. https://github.com/denoland/celld↩
- celld Cloudflare compatibility. https://celld.dev/docs/cloudflare-compat/↩
- Deno security and permissions. https://docs.deno.com/runtime/fundamentals/security/↩
- Cap’n Web — WebSocket transport,
RpcTransportinterface, Deno support. https://github.com/cloudflare/capnweb↩ - Deno configuration (
deno.json, import maps, tasks,--config). https://docs.deno.com/runtime/fundamentals/configuration/↩ - Deno — MIT-licensed single static binary; official macOS/Linux/Windows releases. https://github.com/denoland/deno↩
- Puppeteer
PageAPI. https://pptr.dev/api/puppeteer.page↩ - Astral — puppeteer/playwright-like browser automation for Deno. https://jsr.io/@astral/astral↩
- Restate durable execution — journal + replay. https://docs.restate.dev/concepts/durable_execution↩
- Cloudflare Workflows —
step.do/step.sleep/step.waitForEvent. https://developers.cloudflare.com/workflows/↩ - Restate SDK actions —
ctx.run, durable timers, awakeables. https://docs.restate.dev/foundations/actions↩ - Electron
powerMonitor—suspend/resumeon macOS, Windows, and Linux. https://www.electronjs.org/docs/latest/api/power-monitor↩ - Durable Object alarms — one per object, at-least-once execution. https://developers.cloudflare.com/durable-objects/api/alarms/↩