Octov0.11.7
Platform

Dev Runs

The editor's Run on the platform: a pod of its own, a stable hostname, and reload on save.

Pressing Run in the platform's editor starts a dev run: a Kubernetes pod that runs your integration while you work on it, with its own hostname and its own log stream. It is not a deployment: nothing is tagged, nothing is versioned, and it disappears when you stop using it.

Why it is a pod

The standalone editor spawns octo run --watch as a child of the web app and streams its output. The platform used to do the same, and it breaks the moment the platform runs more than one replica. With no session affinity, the child process, its port and its log buffer all live in one replica's memory, while the next status poll, log stream or Stop lands on a replica that has none of it and reports nothing running.

A pod of its own removes that state rather than replicating it. The platform app holds nothing about your run: every operation asks the orchestrator, which reads the cluster, so any replica can answer anything.

The one-shot debug operations (invoking a single flow, evaluating a CEL expression, running test suites) still execute inside the platform pod, from the YAML in the request. They finish before the response does, so they still run exactly what is on your canvas.

What a run is made of

Deployment octo-dev-{id}, one pod, two containers
  ├─ dev sidecar   owns the workspace: pulls your definition and resources
  │                from the orchestrator, writes them where the runtime watches
  └─ runtime       octo run --config /workspace/integrations --watch
                   the standalone build, no cluster credential of any kind

The two containers share an emptyDir. The sidecar's only peer is the orchestrator, in both directions: it receives reload commands from it and fetches the definition from it. It never talks to the Kubernetes API.

The runtime container is the standalone build of octo, the same one you get from octo run on your laptop, not the cluster build the orchestrator uses for deployments. A dev run therefore has no leader election, no cluster queues, no log shipping and no KV store, which also means stopping one leaves nothing behind to clean up.

It runs what you saved

This is the one place the platform editor deliberately behaves differently from the standalone one:

StandalonePlatform
What runsthe editor's bufferthe saved definition
Reload triggertyping (debounced ~2s)saving
Who moves the configthe app writes the filethe pod fetches it

Because the sidecar fetches your integration from the orchestrator, the running app reflects what is stored, so an unsaved edit is not running yet. Save, and the reload follows automatically: the orchestrator notices the write, tells the sidecar, and the sidecar re-fetches and rewrites the config the runtime is watching. Editing an env file or another resource reloads it too.

Two consequences follow from firing the reload where the write lands rather than in the editor. Every writer is covered, not just the canvas: a save from the integrations list, an MCP agent calling update_flow, or an API-key client all reload a running dev run. And a failed reload never fails your save, because the notification is best-effort and happens after the write is committed. A save that succeeded while a pod was unreachable leaves a stale running app rather than losing your work.

Run also requires a saved integration. There is nothing for the pod to fetch for a draft, so the editor says so instead of starting something that would run nothing.

Its address is stable

A networked integration (one whose definition declares HTTP_PORT) gets a hostname of the form https://<hash>.<your apps domain>, shown in the console header once the pod is ready. The label is eight lowercase characters, derived from a keyed hash of you and the integration and nothing else.

Three things follow. Stop and Run again and you get the same URL, so a webhook you register with Stripe or GitHub against it keeps working across restarts. Renaming the integration does not move it, because the hostname depends on no field you can edit. And two integrations are always two addresses, even if one is a copy of the other.

The URL is unguessable, not private. It is a hash keyed by an installation secret, so nobody can derive it, but anyone who has it can reach your dev run for as long as the integration exists, and no rename will rotate it. Treat it as a shared secret, not as access control: do not put anything behind it that you would not put behind a bearer token.

Sharing, and how a run ends

A dev run belongs to you and one integration. Two tabs on the same integration share one run: the second attaches to the existing pod, and a reload triggered in one tab is visible in the other. An agent shares it too, so an MCP client's run_integration for an integration you have open reaches the same pod. See Platform MCP.

Three ways a run ends, and only the first is something you do:

StopImmediate. The pod and its endpoint go away.
IdleA run nobody has touched for the configured timeout (DEV_RUN_IDLE_TIMEOUT, an hour by default) is reaped. This is the only bound on how many pods an editing session leaves behind.
Its integration is goneThe sidecar's next fetch finds nothing, so it asks to be torn down and exits. Until something triggers that, a deleted integration's pod can keep serving until the idle timeout collects it.

Where the state lives

There is no dev_runs table, and nothing about a run is written to Postgres:

What you might expect storedWhere it actually is
The run's idDerived from (user, integration). Stable, so it needs no storage.
Its owner and integrationLabels on the workload, which is also how they are queried.
Its hostnameDerived from the same pair. A pure function needs no row to be stable.
Its listen portNot a variable: the platform injects a fixed one. A pod owns its own network namespace.
Whether it is runningIts existence. Gone means stopped, which is the one answer that cannot be stale.
When it was last usedThe octo.dev/last-activity annotation, which the reaper reads.

A table mirroring the cluster would be a second source of truth, and the two drift the moment a node evicts a pod while the row still says running. Two costs follow from having none. There is no history of a stopped run: the platform can show you what is running, not what once ran. And nothing is queryable outside the cluster. No SELECT will tell an operator what dev runs exist; the answer comes from the orchestrator or from kubectl get deploy -l octo.dev/dev-run-id.

Turning it on

Dev runs are off unless the Helm chart enables them, because a single-replica self-hosted install has nothing to gain. See Dev runs in the Helm chart for the values, and in particular why the HMAC key (DEV_RUN_HASH_SECRET) must be generated once and kept. Rotating it changes every dev-run hostname, which silently breaks any webhook registered against one.

  • Architecture: where the platform app, the orchestrator and this fit together
  • Deployments: the other way an integration runs, versioned and long-lived
  • Running flows: the Run button itself, and the console
  • Platform MCP: the same runs, driven by an agent

On this page