# Workflows: small, auditable, resumable pipelines

Most agent work in this package needs no graph. `deliberate()`, `agent_experiment()`, and the preset pipelines have their own interfaces, and you should use them. The workflow runtime is for what those do not cover: a procedure with branches, loops, a human checkpoint in the middle, or a run long enough that you stop and resume it tomorrow. The runtime is small. A node is a function of state; an edge is an optional condition; the state is a plain list you can read. There is no distributed execution and no DSL beyond `add_node` and `add_edge`. A procedure you can read in full is one you can audit.

    library(LLMRagent)
    cfg <- LLMR::llm_config("groq", "openai/gpt-oss-20b", temperature = 0.3)

## State in, state out

A node receives the shared state and returns a new state. That is the whole contract. Here is a two-step procedure that drafts an answer and then tightens it, with the state carrying the work from one step to the next:

    wf <- agent_workflow("draft_then_polish") |>
      add_node("draft", function(state) {
        a <- agent("Drafter", cfg)
        state$draft <- a$reply(paste("Draft two sentences on:", state$topic))
        state
      }) |>
      add_node("polish", function(state) {
        a <- agent("Editor", cfg)
        state$final <- a$reply(paste("Tighten this to one sentence:", state$draft))
        state
      }) |>
      add_edge("draft", "polish")

    run <- run_workflow(wf, input = list(topic = "why peer review is slow"))
    run$state$final

`run_workflow()` returns a record of what happened: the sequence of nodes, the state at each step, and a status. Because every node’s state is hashed, the run is checkable, which is what makes the next two features possible.

## Branches and loops

An edge can carry a condition. The runtime takes the first edge whose condition holds, so you can branch on what a node produced, or loop until something is good enough. Loops are bounded by `max_steps`: a runaway procedure stops rather than spends, raising a typed error you can catch.

    wf2 <- agent_workflow("until_short_enough") |>
      add_node("write", function(state) {
        a <- agent("Writer", cfg)
        state$tries <- (state$tries %||% 0L) + 1L
        state$text <- a$reply(paste0("Write a one-line summary of: ", state$topic,
                                     " (attempt ", state$tries, ")"))
        state
      }) |>
      add_node("done", function(state) state) |>
      add_edge("write", "write", when = function(state) nchar(state$text) > 120 && state$tries < 3) |>
      add_edge("write", "done", when = function(state) nchar(state$text) <= 120 || state$tries >= 3)

    run2 <- run_workflow(wf2, input = list(topic = "the history of the printing press"),
                         max_steps = 10)
    run2$state$tries   # how many attempts it took

## Checkpoints, resume, and fork

Give `run_workflow()` a `checkpoint_dir` and it writes the state after every node as it goes. If the run fails partway, from a network interruption or a model timeout, you resume from where it stopped rather than starting over, and completed nodes do not run again.

    dir <- file.path(tempdir(), "long_run")
    run3 <- run_workflow(wf, input = list(topic = "open access publishing"),
                         checkpoint_dir = dir)
    # had it failed, resume_workflow(run3, wf) would continue from the last good node

`fork_workflow()` branches an existing run at a chosen point, so you can try a different continuation from a shared prefix without recomputing it – useful for asking “what if the editor had been stricter” without paying for the draft twice.

## Replay verification

A run records a state hash at each step. `replay_run()` re-executes the procedure and compares. The comparison accounts for nondeterminism: a deterministic node must reproduce its hash exactly, while a model node is checked for design identity rather than identical text, because a model does not return the same words twice. A divergence names the first step that differs.

    rp <- replay_run(run, wf, verify = "structural")
    rp$steps[, c("node", "replay_match")]

Use `verify = "strict"` when the whole procedure is meant to be deterministic and any difference is a problem. Use the default `"structural"` when model nodes are involved and you want to confirm the structure of the run without demanding identical wording. Either way, a tampered checkpoint is caught: the hash will not match.

## When to reach for this

If `deliberate()` or `agent_pipeline()` already does what you need, use them; the workflow runtime is not a replacement and `workflow_from_pipeline()` exists only to show that the engine can express them. Reach for `agent_workflow()` when you need a branch, a loop, a human gate, or durability across a long run. The added setup adds checkpointing, resumption, forking, and replay verification: a pipeline you can leave and return to, and one a reader can verify ran the way you say it did.