Hosts & deployment

Flowdrome splits management from execution:

  • The Nucleus is the control plane. Workflow registry, credentials vault, users & roles, the host fleet, deployments, the approvals inbox. It never executes a workflow.
  • A host is the data plane: the host agent running as its own process — usually on a machine of its own — with the same engine the editor uses embedded inside it. Deployed workflows are served by that engine. Hosts keep running if the Nucleus is unreachable; they dial home for secrets at run start and for management actions.

The split is the architecture’s load-bearing wall. The control plane can restart, upgrade, or sit on the other side of a flaky link without taking a single workflow down — and a host can be a 300 MB live-ISO VM that does nothing but run apps. The screen-by-screen tour of both sides (fleet view, host dashboard, approvals inbox, run drawer) is the Hosts guide.

The built-in host

A Nucleus with no host attached could manage workflows but never run one, so every install started with a second machine before it could do anything. It no longer does: the Nucleus supervises a host of its own, started with it and enrolled over loopback, listed in Hosts as Built-in host. One container, one CT or one executable is a complete Flowdrome.

This does not soften the split above — it is a separate process (the same flowdrome-host executable, dropped beside the Nucleus) with its own data directory, its own engine and its own port (4820, the Nucleus’s port plus 20), enrolled through the same path as any other host on a join token the Nucleus mints for it. The Nucleus still never executes a workflow; it just no longer requires you to supply the machine that does. Turn it off with FLOWDROME_BUILTIN_HOST=0 when you want execution only on machines you chose.

It binds whatever interface the Nucleus binds — exactly as exposed as its parent, never more — but what it advertises is loopback, because the Nucleus calls it from the same box. The dashboard knows that means “the machine the Nucleus is on” and shows you the address you are browsing on instead, with the host’s port kept. The variables that change any of this (FLOWDROME_BUILTIN_HOST_PORT, _BIND, _ADVERTISE) are in Install.

Enrolling a host

Enrol a host when you want workflows executing somewhere other than the Nucleus’s own machine. Enrolling is never a prerequisite for deploying: the built-in host above is already a target.

Install a host (a Docker image or a Proxmox LXC template from the releases page, the live ISO appliance, or the bare single-file agent), then connect it: Hosts → Join token in the Nucleus mints one; the agent takes it via CLI (--join), environment (FLOWDROME_JOIN_*) or its GUI Connect to Nucleus screen. Authorized hosts appear in the fleet view, grouped by environment, with live status, an open-host link, and per-host deployments. De-authorizing a host stops it from receiving anything.

Deploying a workflow

From a workflow’s Deploy action, pick a host — that’s the whole decision. The Nucleus pushes the exact flowdrome.workflow.v1 document you tested (no conversion, no compile step), stamped with its saved version and deploy time, plus a scoped resolve token for secrets leasing. The host persists the deploy envelope (it survives restarts) and registers the workflow with its embedded engine — the unified hosted runtime: one engine and one worker pool per host, shared by every workflow deployed to it.

The host’s serve layer takes over the triggers: HTTP webhooks, schedules (interval and cron), HTML forms, web chat, WebSocket, Telegram (long-poll and webhook), Slack events, WhatsApp, and the Table trigger’s change-feed poller. Runs execute on the embedded engine with the same envelope capture you saw in the editor. Stopping a deployment is intentional and distinct from a crashed one; removing it uninstalls the document.

x64 note: the host’s engine worker is compiled with Bun’s standard build, which requires AVX2. On Proxmox set the VM CPU type to host — a kvm64-class vCPU kills it at launch (exit 132).

Sub-workflows come along

A workflow that runs sub-workflows installs self-contained. On deploy the Nucleus walks the whole dependency tree and:

  • Checks everything exists first — every sub-workflow it uses (transitively), plus the variables and credentials it and its subs reference. Anything missing blocks the deploy with the exact list, so a workflow never installs only to fail on its first run.
  • Inlines the sub-workflows into the deployed document. Each Execute Workflow node is expanded into the master graph, so the host serves one workflow: every node runs — and is tracked — in a single run history, and the sub-workflows never appear as separate deployments cluttering the fleet.

The Deploy action previews what an install brings along (“…brings along 3 sub-workflows, embedded to run inline”). Secrets are still never baked in — the host dial-homes for their values at run start; the dependency check only confirms they’re defined.

The same self-contained artifact comes from Export → Self-contained (or GET /api/workflows/{id}/export?bundle=1) — a single file that installs anywhere with nothing else to bring.

Deploy carries a frozen copy

Sub-workflows aren’t the only dependencies that ride along. A deploy walks the workflow for the tables it reads or writes and the interfaces whose pages run it, and freezes a copy of each:

  • Tables deploy with their schema pinned at the deploy-time version. The host serves rows against that frozen schema, so a column dropped or retyped in the Nucleus tomorrow can’t break a running app.
  • Interface pages that call the workflow deploy with the workflow, at their current content version, and are served from the host.

The deploy manifest lists all of it up front — the same “checks everything exists first” gate that covers sub-workflows now includes tables and interfaces, each with the exact version being pinned:

GET /api/workflows/{id}/dependencies
  → { ok, subWorkflows[], variables[], credentials[],
      tables:[{id,name,version,present}], interfaces:[{id,title,slug,version}] }

Editing the live table or interface afterwards mints a new version (its version history records it), but active deployments keep serving their pinned copy — they don’t drift out from under you. The change reaches a host only when you redeploy. Until then the table/interface’s Where used view flags each lagging deployment stale, so the impact of a schema or content edit is visible before you commit it — the same version-badge drift you see for the workflow document itself.

Or skip the host: package a unit

Deploying puts a workflow onto a machine you already run. The other shape turns it around — the workflow becomes the machine.

Package as unit (Workflows screen) emits one finished artifact: a docker load-able image or a Proxmox CT template, carrying the host runtime, the dashboard and every workflow the selection needs. Copy it over, install it, done — there is no build step, and nothing else has to be present.

Deploy to a hostPackage as a unit
The machineone you already runthe artifact is the machine
Arrives asa push from the Nucleusa file you copy
Credentialsleased from the Nucleusleased, or sealed inside
Runs air-gappednoyes, when sealed
Updating itre-deployrebuild and reinstall

A unit is still a host underneath — same engine, same console, same start/stop/pause — so everything below about serving, pausing and run history applies to it unchanged. The Package as a unit guide covers the sealed/connected choice, access modes and what a unit can tell you about itself.

What a host exposes on its port

A host — enrolled, packaged as a unit, or built into the Nucleus — serves its dashboard, its API and every deployed workflow’s triggers from one port: 4801 for a standalone host, 4820 for the Nucleus’s built-in one. The Docker image, the Proxmox LXC template and the live ISO all bind 0.0.0.0, so the box answers on every interface it has; the bare executable defaults to 127.0.0.1 and widens only when FLOWDROME_HOST_HOST says so (the built-in host follows the Nucleus’s binding instead, or FLOWDROME_BUILTIN_HOST_BIND). Put a host on a network you trust, or behind a proxy that fronts it.

Token mode is the default, and a packaged unit always pins a token. It gates mutations and run content — not the whole box. Without any credential, a caller who can reach the port reads:

EndpointWhat it returns
GET /healthzliveness
GET /, /assets/*the dashboard shell
GET /api/confighost or unit name, environment, whether open mode is on, whether this is a sealed unit
GET /api/bootstraphost id, boot time, the data-root and apps paths, the agent’s kind and version
GET /api/enrollmentwhether it is enrolled — and if it is, its Nucleus URL and instance id
GET /api/appsevery deployed workflow’s name, version, status and routes (method and path), its run and failure counts, when it last ran, its schedule, and any pending approval gates with their title and description
GET /api/engine/workflowsthe same workflows in the serve layer’s own shape — name, version, when they were deployed, and every route, form, chat, WebSocket and bot path
GET /api/apps/{id}/metricsrun counts, average and max latency, success rate, the history series, the slowest steps by name
GET /api/resourcesthe box itself — OS and architecture, uptime, CPU count and load, total/free memory and disk, and the data path
GET /api/metrics/exportthe host id and environment, plus the recorded metric samples the Nucleus scrapes
GET /api/settingsthe engine knobs, the resolved data/workflow/log paths, the telemetry URL
GET /api/serve/runs/{id}one run’s outcome — see below
the deployed routes themselveswhatever the workflow answers

Metrics answers an anonymous caller with the charts and not the runs: the runId, each run’s error text and the latest run’s console logs are stripped out. Sending the token (or signing in with your Nucleus credentials, on an enrolled host) restores them.

Everything else needs the host access token as Authorization: Bearer <token> — or, on an enrolled host, a signed-in Nucleus user’s token, which the host forward-validates against its Nucleus:

  • GET /api/engine/runs/{id} — the node-by-node run detail: every node’s input and output.
  • GET /api/engine/workflows/{id}/definition — what the box actually runs.
  • GET /api/credentials-in-use — the credential names this box needs.
  • GET /api/apps/{id}/approvals, and approve/reject — deciding a parked run is a mutation of its outcome.
  • Every mutation: deploy, remove, start/stop/pause/resume, settings, disconnect, run-a-document.

GET /api/serve/runs/{id} is open on purpose. It is how a webhook caller collects the result of the run it started, so the unguessable run id is the capability that scopes it — and what comes back is the caller view (status, output, error), never the execution trace. Treat a run id like a bearer token for that one run’s outcome.

One mutation is open in one window: while a host is not yet enrolled and no token is pinned, POST /api/enroll accepts anyone — the first-boot setup path, where the join token is the real boundary. A packaged unit pins its token at build time and never has that window; a bare agent has it from first boot until it enrolls.

The built-in queue broker (POST /mq/*) goes the other way: with no FLOWDROME_MQ_TOKEN set it refuses every caller that is not on loopback.

An enrolled host is also a proxy hop to its Nucleus for a couple of read paths (/api/nucleus-apps…, which is how its dashboard folds in the Nucleus’s own apps). It forwards whatever bearer the caller sent, so the Nucleus’s own auth decides the answer — and by default that means an anonymous visitor reads as the Nucleus’s built-in view-only principal. FLOWDROME_REQUIRE_AUTH=1 on the Nucleus closes it. A standalone host or unit has no Nucleus and answers an empty list.

Watching the fleet

  • Workflows view — each workflow row expands to its deployments across hosts (matched by workflow id, so renames don’t orphan deployments), with status, a version badge (v3 in teal when the deployment matches the current saved version, orange when it has drifted behind a newer save), and a logs drawer streaming output through the Nucleus.
  • Hosts view — fleet health at a glance; each host’s own dashboard shows its workflows, queues and the run ledger, with per-workflow CPU, memory and network telemetry on an expandable strip in each row rather than a separate tab.
  • Approvals inbox — every pending approval gate across every authorized host, in the Nucleus header, with approve/reject in place.
A host dashboard with running apps and a pending approval
A live host's own dashboard: six running apps, a pending approval panel, and the selected app's run statistics — the data plane managing itself.

The host upgrade story

Host state lives in one data directory; the agent is replaceable around it. The built-in host is upgraded by upgrading the Nucleus — it is a binary inside the same image or CT, and its data lives under the Nucleus’s own data directory (builtin-host/), so it rides the same volume.

  • Bare exe / Docker: replace the binary or image, keep the data directory / volume.

  • Proxmox LXC: run flowdrome-upgrade <new-template.tar.gz | url> inside the container. The template doubles as the upgrade artifact, and the helper is baked into every CT — host and Nucleus alike — at /usr/local/bin/flowdrome-upgrade. From the Proxmox node itself that is pct push <vmid> <file> /root/u.tar.gz && pct exec <vmid> -- flowdrome-upgrade /root/u.tar.gz.

    It replaces /app rather than extracting over it: merging leaves every hashed UI asset from every past build on disk, and index.html is the one file with no hash in its name — a cached tab goes on loading the previous release’s bundle, silently, with no error anywhere. Swapping the directory makes a stale cache 404 and reload. The old /app is kept at /app.previous until systemctl confirms the new service came up active, and the upgrader installs the new version of itself last, only once that has happened. /data is never touched.

    Or, with /data on its own mount point, create a new CT from the new template and pct move-volume the data volume across.

  • Live ISO appliance: the OS is the ISO — upgrading is swapping the attached ISO and rebooting. At boot the appliance adopts its data disk by label:

    1. A filesystem labelled FLOWDROME_DATA → mounted as the data directory. Never reformatted.
    2. Otherwise, the first provably blank disk (no filesystem signatures, no partition table; removable/optical/read-only devices excluded) → formatted exactly once, labelled FLOWDROME_DATA.
    3. No candidate disk → the host runs ephemeral and says so on the console.

    Host identity, apps and run data ride the data disk across ISO swaps. OS-level changes (including passwords) live on the boot overlay and reset every reboot — that is the appliance contract.

Scale expectations

The management plane is light: a host comfortably manages 100 deployed workflows on its shared runtime (monitoring round-trips under 50 ms, restarts in the hundreds of milliseconds). Deploys are pushes of a JSON document — provisioning many hosts is bound by your network, not by a build step.