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: 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).

PhaseScopeGateState
ADRs + this notemanual read-through⏳ awaiting read-through
1Deno runner + WS bridge + Runs panel + ported examplesall seeded examples passnot started
2celld spike (checklist below)every item passesnot started
3SDK + workflows, individual mode, durable-execution v1see roadmapnot started
4Cross-client (shared bucket)see roadmapnot 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:

  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.
  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; 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).

  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 — the everyday case under Mode A client-supervised operation; cron emulation depends on this.
  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 (informs specific 5’s protocol choice).
  10. 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.
  11. Alarm multiplexing under load — granularity, drift, re-arm-in-handler reliability with many pending timers.
  12. 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: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).
  • 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) 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).
  • 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.ts for 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