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: celld is the workflow-engine substrate; 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
Design only; nothing implemented (2026-08-06).
| Phase | Scope | Gate | State |
|---|---|---|---|
| — | ADRs + this note | manual read-through | ⏳ awaiting read-through |
| 1 | Deno runner + WS bridge + Runs panel + ported examples | all seeded examples pass | not started |
| 2 | celld spike (checklist below) | every item passes | not started |
| 3 | SDK + workflows, individual mode, durable-execution v1 | see roadmap | not started |
| 4 | Cross-client (shared bucket) | see roadmap | not started |
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
All 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.
- 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; hub-and-spoke is the first cross-client topology. celld runs as a client-supervised child (Mode A) — every install is an engine node while a client is open; one always-open client on an always-on machine is the de facto hub. Clarified in the same exchange: workflow code executes in the celld daemon, never in a webview — webviews only execute the browser portion of capability steps (ADR-0023, specifics 1/6/8).
Research summary
celld[^1] (denoland/celld[^2]): 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. Compat[^3]: 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.
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" contract[^4]. Runtime behavior on Deno (via npm:capnweb) remains a
Phase 1 verify.
Deno permission model[^5]: 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 + --config[^6].
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-core[^7] (heavy; Node-compat risk on Deno; its Electron gaps are what the current code hacks around) vs astral[^8] (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 + replay[^9], 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 Workflows[^10] 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/awakeables[^11] and
Cloudflare’s steps, so all three engines remain retarget candidates. DO alarms — the primitive the
v1 emulation rides — are one-per-object with setAlarm overwrite and at-least-once handler
execution[^12].
celld spike checklist (the Phase 2 gate)
Each item has a pass criterion; any failure is a no-go and flips ADR-0023 toward its Restate fallback (Option B).
- 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 — the everyday case under Mode A client-supervised operation; cron emulation depends on this.
- 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 (informs specific 5’s protocol choice).
- Crash-resume (durable-execution v1 go/no-go) — 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.
- Cross-node takeover, run locally — two celld nodes on different ports sharing the one MinIO bucket; kill the owner mid-run. Pass: the second node acquires the run cell’s lease, materializes its SQLite from the bucket, pending alarms fire on the new owner, and the run resumes without re-executing finished steps. This proves the “any client can pick up a failed run” claim without two machines; measure takeover latency.
Open questions
- Deno binary distribution (require-on-PATH is the ADR-0022 decision; managed download later?).
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). - Hub↔client protocol after spike item 9 (frames vs capnweb-in-the-hub).
- MinIO licensing/alternatives — AGPL-3.0 and the upstream repo is archived (verified 2026-08-06)[^13]; watch S3-compatible alternatives and any celld filesystem-bucket backend.
- Editor TS tooling for script authors inside an Atom-lineage editor (
deno lsp?). - Windows support (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).
- Journal retention/GC for completed runs.
- Parallel steps (v1 is sequential).
- Home of the shared step-primitive module (imported by both the runner lib and the SDK).
- How the SDK expresses capability-request affinity (per-step option? inferred from the capability used?).
- Whether a non-owner node proxies requests to a cell’s owner (webhook ingress via any node vs owner-only).
- Mode A supervision details — engine shutdown grace on app quit mid-run (SIGTERM lets the current step journal; resume happens on next launch).
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 — celld spike: the checklist above. Gate: every item passes → ADR-0023 flips Active. Any failure → supersede toward the Restate fallback (Option B — the team’s proven stack), with bespoke/Cloudflare as further options.
- Phase 3 — SDK + workflows, individual mode, including durable-execution v1 (steps, retries/backoff, durable sleep, awakeables, crash-replay) under Mode A (client-supervised engine). Gate: webhook + schedule + manual all fire while a client is open; a schedule due while everything was closed fires via catch-up on next launch; one browser-callback workflow end-to-end; a workflow survives a daemon kill mid-run and completes without re-executing finished steps.
- Phase 4 — cross-client (shared bucket + Tailscale). Gate: two real devices; a run started on device A resumes on device B after A goes offline — journal, retry timers, and parked awakeables intact; a device-agnostic browser step executes on B’s connected client while a device-pinned one stays parked until A returns; coordinator failover; no data loss.
Docs housekeeping
- Nav entries added to
src/lib/data/docs.tsfor ADR-0022, ADR-0023, and this note. - 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.
[^1]: celld documentation. https://celld.dev/docs/ [^2]: denoland/celld — “self-hosted, distributed Durable Objects”; announcement: https://x.com/rough__sea/status/2085001943693549887. https://github.com/denoland/celld [^3]: celld Cloudflare compatibility. https://celld.dev/docs/cloudflare-compat/ [^4]: Cap’n Web — WebSocket transport, RpcTransport interface, Deno support. https://github.com/cloudflare/capnweb [^5]: Deno security and permissions. https://docs.deno.com/runtime/fundamentals/security/ [^6]: Deno configuration (deno.json, import maps, tasks, --config). https://docs.deno.com/runtime/fundamentals/configuration/ [^7]: Puppeteer Page API. https://pptr.dev/api/puppeteer.page [^8]: Astral — puppeteer/playwright-like browser automation for Deno. https://jsr.io/@astral/astral [^9]: Restate durable execution — journal + replay. https://docs.restate.dev/concepts/durable_execution [^10]: Cloudflare Workflows — step.do/step.sleep/step.waitForEvent. https://developers.cloudflare.com/workflows/ [^11]: Restate SDK actions — ctx.run, durable timers, awakeables. https://docs.restate.dev/foundations/actions [^12]: Durable Object alarms — one per object, at-least-once execution. https://developers.cloudflare.com/durable-objects/api/alarms/ [^13]: MinIO — AGPL-3.0; upstream repository archived. https://github.com/minio/minio