markov

Step-Through Debugging

markov run can pause at breakpoints, report the variables the next step will see, and continue one step at a time. The Markov IDE uses this, but the protocol is plain JSON and works from any program or a terminal.

A paused run is held in memory: the engine blocks the step and waits for a command, so for_each and loop state are intact. This is separate from a gate’s pause action, which stops the process and is resumed later with markov resume.

Starting a debug run

Breakpoints and --step need --control stdin, so a paused run can always be continued:

markov run pipeline.yaml --control stdin \
  --callback jsonl:///tmp/events.jsonl \
  --breakpoint '{"workflow":"main","step":"deploy"}'
Flag Description
--control stdin Read debugger commands from standard input, one JSON object per line.
--breakpoint '<json>' A breakpoint object (repeatable).
--breakpoints-file file A JSON file holding an array of breakpoint objects.
--break workflow.step Shorthand for typing in a terminal (see below).
--step Pause before every step.

Every breakpoint is resolved against the workflow before the run starts. An unknown workflow, step, section or phase fails immediately and lists the real names, so a typo can never silently fail to fire. Unknown JSON fields are rejected as well.

Pauses, resumes and other debugger output are sent to the run’s callbacks as debug events; without a --callback they are only logged.

markov resume accepts the same flags, so a resumed (or rewound) run can be debugged too.

Breakpoints

A breakpoint is a JSON object. Names are never parsed out of a string, so workflow and step names can contain any characters (spaces, dots, colons, brackets).

Field Required Description
workflow yes Workflow name.
step yes Step name.
section no steps (default), rescue, or always.
iteration no Only this for_each key. Without it the breakpoint applies to the step as a whole: a for_each step pauses once, before the loop starts.
phase no before (default: the step is about to run) or after (it just completed; the pause carries its output).
when no A condition in the same syntax as a step’s when, evaluated against the variables the step sees. If the condition itself fails to evaluate, the run pauses and reports the error rather than silently never firing. Conditions must be side-effect free; Jev functions are not available in them.
skip no Ignore the first N hits.
log no A template. The breakpoint becomes a logpoint: the rendered message is reported in a logpoint event and the run continues.

A breakpoint only fires for a step that will actually run: a step skipped by its own when:, or replayed as already completed by markov resume, does not pause. A step in a sub-workflow is matched by the sub-workflow’s name; the run_id of the event identifies which call it belongs to (a sub-workflow run is named <parent run id>-<call step name>).

The --break shorthand

--break main.build is for typing in a terminal. It is not split on the dot. Every real workflow name is tried as a prefix and the remainder must be one of that workflow’s steps, so my flow.v1:x.step one.a:b [c] works. If nothing matches, the error lists every known workflow.step. If two readings are both valid (a workflow a with a step b.c, and a workflow a.b with a step c), the command refuses and shows the --breakpoint form to use.

Commands

One JSON object per line on standard input.

Command Fields Effect
set_breakpoints breakpoints Replace the whole breakpoint list. Allowed at any time. Answered with a breakpoints_set event listing resolved and unresolved entries.
continue   Run until the next breakpoint.
step   Pause before the next step that starts, in any workflow or iteration. While running, it requests a pause at the next step.
until workflow, step, optional section, iteration Continue and pause before that step (one shot).
evaluate id, expression While paused, render the expression against the paused variables (n + 1 and {{ n + 1 }} are equivalent). Answered with an evaluated event carrying the same id.
terminate   Stop the run. The step is recorded as failed and the run can be continued with markov resume.

Only one pause is active at a time. With concurrent for_each iterations, the others wait their turn; a continue drops step-mode pauses queued behind it. Use concurrency: 1 on the step for a deterministic order.

If standard input closes while the run is paused, the run is aborted (failed, resumable) so nothing is left blocked; if it closes while running, debugging is simply switched off.

What a pause contains

See debug events. The paused event has workflow_name, step_name, and data with reason (breakpoint, step, until, condition_error), phase, section, iteration, step_type, variables and, for after, output. Variables are the JSON form of the context the step sees; very large contexts are replaced by their key list.

Example session

markov run examples/debugging.yaml --control stdin \
  --callback jsonl:///tmp/events.jsonl --break main.greet
{"cmd":"evaluate","id":"w1","expression":"level * 6"}     # -> evaluated: 42
{"cmd":"step"}                                             # pause before the next step
{"cmd":"until","workflow":"main","step":"done"}            # run to the final step
{"cmd":"continue"}

Limits