This document describes how devloop behaves at runtime beyond the
schema in configuration.md.
This reference is also available in the CLI with:
devloop docs behaviordevloop is moving toward a pure core with an imperative shell.
- workflow progression is modeled as explicit state plus effect requests
- startup orchestration, watch-triggered workflow scheduling, maintain ticks, shutdown, and process-supervision decisions are also modeled as explicit state plus effect requests
- the runtime interprets those effect requests to perform process control, hooks, sleeps, logging, and persistence
- workflow and runtime effect interpreters sit behind replaceable adapter boundaries, so orchestration can be tested against mocks without requiring live subprocesses or file watchers
The remaining imperative shell is now mostly the concrete adapter layer
that talks to Tokio, notify, child processes, HTTP probes, and the
filesystem.
When devloop run starts, it:
- loads and validates
devloop.toml - resolves
root,state_file, command paths, and relative working directories - loads the session state file into memory
- if external events are configured, starts a localhost event server
- starts any processes with
autostart = true - runs each workflow named in
startup_workflowsin order - starts watching the configured
root
The in-memory session state is authoritative for the running process.
Edits made directly to the JSON file while devloop is running are not
merged back into the live session.
devloop watches the configured root recursively.
- Only relevant file-system events are considered.
- Events are batched for
debounce_ms. - Matching changes are grouped by workflow name before execution.
- Each workflow receives the set of changed relative paths that matched it during the debounce window.
If multiple watch groups map to the same workflow, their matched paths are merged for that workflow run.
Workflows run step by step, in order.
- A step must finish successfully before the next one begins.
run_workflowexecutes another named workflow inline.- Recursive workflow graphs are rejected at config-validation time.
write_staterenders{{state_key}}templates against the current in-memory session state.logalso renders templates against the current session state before emitting output.
If any step fails, the workflow fails immediately and devloop run
returns an error.
Managed processes are long-running child commands.
start_processis a no-op if the named process is already running.restart_processstops the child, then starts it again.wait_for_processwaits on the configured readiness probe, not just on successful spawning.restart = "always"restarts a child after any exit unlessdevloopis shutting down.restart = "on_failure"restarts only after unsuccessful exit.restart = "never"never restarts automatically.- Managed child processes inherit the ambient environment unless the
process config explicitly overrides individual variables such as
env.RUST_LOG.
Liveness probes are checked on the configured interval while the process is running. If a liveness probe fails and the restart policy allows it, the process is restarted.
Hooks are one-shot commands executed inside workflows.
- Hooks run to completion before the workflow continues.
- Hook stdout and stderr are captured fully, then rendered with a source
label if
hook.<name>.output.inheritis enabled. - Hook output defaults to
body_style = "dim"so helper-command output is visible but visually secondary. - Hook capture is independent of hook output rendering.
capture = "text"trims stdout and stores it instate_key.capture = "json"parses stdout as a JSON object and merges it into the session state.- A non-zero hook exit status fails the workflow after any captured stdout and stderr have been rendered.
Hooks can also be observed outside workflows.
- If
hook.<name>.observeis configured, the runtime polls that hook on the configured interval during normal maintenance ticks. - If running the hook changes session state, the configured observe workflow is scheduled immediately.
- If the hook leaves session state unchanged, no follow-up workflow is run.
Observed hooks remain useful as a cheap fallback when push integration is not worth the extra control surface. For lower-latency and less noisy event flows, prefer external events instead.
If event.* config is present, devloop starts a localhost HTTP server
for constrained event ingestion.
- Each configured event maps to one fixed session-state key and one fixed workflow.
- Child processes receive the event URLs and bearer token in their environment.
- Posting the same value again does not rerun the workflow.
- Posting a new accepted value updates session state first, then schedules the configured workflow immediately.
- Invalid tokens are rejected.
- Values that fail the configured regex pattern are rejected.
devloop uses a line-oriented, pipe-based output model.
- Terminal-native subprocess UIs are a non-goal.
- Child stdout is forwarded to
devloopstdout. - Child stderr is forwarded to
devloopstderr. devloopengine and process logs are emitted throughtracing.- Managed-process and hook output is source-labeled as
[executable process-name]. - Internal
devloopand dependency logs are grouped under[devloop ...]labels so the emitting supervisor remains visible first. - When output color is enabled, labels are colorized per source.
Colorized output is enabled when stdout is a terminal and NO_COLOR is
not set.
body_style = "plain"preserves subprocess body text as-is.body_style = "dim"dims both the inherited source label and body text.- When a subprocess emits ANSI SGR color sequences while
body_style = "dim",devloopreapplies dim after each SGR sequence so the original tint is preserved as much as the terminal allows. - Source-label colors intentionally avoid bright white because it is too visually aggressive in mixed logs.
devloop prefers visibility over terminal redraw semantics.
\ris treated as a visible line boundary.\r\ndoes not double-print.- Output is buffered by line before each write so prefixes do not split mid-line.
- UTF-8 multibyte sequences are buffered before decoding so characters
such as
μsurvive inherited output rendering.
This is meant for readable supervised logs, not PTY emulation.
Long-running processes can write values into session state by matching their inherited output against configured rules.
- Rules run on the raw output stream, line by line.
- Regex extraction uses the configured
capture_group. url_tokenextracts the first token that looks like atrycloudflare.comURL.- State keys configured in output rules are cleared before the process starts.
This is how a process such as cloudflared can publish a readiness
value without wrapper scripts.
HTTP probes succeed on an HTTP success status.
State-key probes succeed when the referenced session-state key exists and is not empty after trimming.
These probe types are used both for workflow waiting and for ongoing liveness checks.
Session state is shared across the running engine, workflows, hooks, and output-derived updates.
rootis written into session state when the engine starts.last_workflowandlast_changed_filesare updated for top-level workflow runs triggered by watches or startup execution.- Nested
run_workflowcalls reuse the same session state without overwriting the top-level change context.
On ctrl-c, devloop:
- marks itself as shutting down
- stops all managed processes
- suppresses further automatic restarts
- exits
- PTY emulation
- full-screen terminal UIs
- byte-exact reconstruction of combined stdout and stderr ordering after a child has already split output across file descriptors