ncode

Desktop docs Workflow language

ncode Desktop Automation

Workflow language

This page is the reference for writing workflows by hand. You do not need it to run workflows, and the assistant can write them for you (/create-workflow); read it when you want to adjust one or understand what it does.

What a workflow is#

A workflow is a small program that runs agents in a fixed, repeatable order: first review, then verify, then report, for example. It is written in Elixir as a single .exs file: a meta map followed by plain calls.

elixir
meta = %{
  name: "summarize-changes",
  description: "Summarize the uncommitted changes in plain words",
  phases: ["Read", "Write"],
  budget: 4,
  args: %{base: %{type: :string, default: "", doc: "Git ref to compare with"}}
}

phase("Read")
files = host(:changed_files, base: args.base)
if files == [], do: complete(%{summary: "Nothing changed."})

phase("Write")
summary = agent("Summarize the changes in these files: #{Enum.join(files, ", ")}", name: "writer")
path = write_report("summary.md", summary || "No summary.")
complete(%{summary: "Wrote #{path}"})

The meta map#

KeyMeaning
namethe workflow's name; it also becomes a slash command
descriptionone line shown in lists
phasesthe phase titles, in order, shown as the run's progress
budgetthe most agents one run may start (1 to 1,024); without it, the workflow agent budget setting applies (128 by default)
max_livehow many of its agents may work at the same time (1 to 64); without it, the max live workflow agents setting applies (16 by default)
argsnamed arguments, each with type, default and doc; the program reads them as args.<name>

Functions#

FunctionWhat it does
phase(title)marks the start of a phase
log(text)adds a line to the run log (up to 500 lines)
agent(prompt, opts)runs one agent and returns its final text, or nil if it failed
panel(items, fun)runs fun for every item at the same time, one agent each, and returns the results in item order (nil for a failed slot)
host(op, opts)reads the project safely (see below)
present?(x)true when x is not nil; filter panel results with it
budget()%{total:, spent:, remaining:} agent slots of this run
fingerprint(value)a stable 16-character fingerprint, for removing duplicates
json_encode(value)the value as compact JSON
write_report(filename, text)saves a report in the run's folder and returns its path
read_report(filename)reads a report back, or nil
last_error()the error of the most recent failed agent call, or nil
integrate(result)merges an isolated agent's changes into the project
await_user(question, opts)pauses the run with a question and returns your answer when you resume; options: offers choices
pause(kind, message)stops the step with a reason, for example pause(:no_progress, "Every reviewer failed")
complete(value)ends the run successfully with value as its result

agent/2 options:

  • schema: a JSON-schema-like map; the agent must answer with a matching object, which you get back as a map;
  • name: the label shown for the agent;
  • capability: what the agent may do: :read_only (the default: read and search), :read_write (may also create, edit, move and delete files), :execute (may also run commands), :all (every tool a workflow agent can have) or :none (no tools: it answers from the prompt alone);
  • model: and provider: (by name) and effort: ("low", "medium", "high" or "max") to run this agent on something other than the workflow's default;
  • isolation: :worktree to give the agent its own copy of the project, and max_turns: to cap how many steps it may take.

Reading the project with host#

A workflow never touches files or the shell directly. It asks host(op, opts), and every answer is recorded so a run can be replayed and resumed exactly:

:changed_files, :git_diff, :git_log, :glob, :files, :subdirs, :read_file, :list_dir, :exists?, :dir?, :grep, :now.

Rules a workflow must follow#

Before a workflow is saved or run, a smoke check parses it, checks its arguments and runs the path they select with stand-in agents. It rejects:

  • direct file-system and shell access: the File module, System.cmd, System.shell, :os.cmd, Path.expand, Path.absname, Path.wildcard (use host instead);
  • anything that differs between two runs: clocks, random numbers, unique integers, Process.sleep (use host(:now) for the time);
  • a path that finishes without starting a single agent, when checked against a real project.

A running script that grows past 512 MB of memory is stopped.

Structured answers with schema:#

Give agent/2 a schema: and it returns a map you can use in the next step, instead of free text. The schema is a map in JSON-schema style: type, properties, items, required, enum. If the agent fails or cannot produce a matching answer, you get nil, so check with present?/1:

elixir
finding_schema = %{
  type: :object,
  properties: %{
    file: %{type: :string},
    title: %{type: :string},
    severity: %{type: :string, enum: ["low", "medium", "high"]}
  },
  required: [:file, :title, :severity]
}

result = agent("Find the riskiest function in lib/payments.ex", schema: finding_schema, name: "scout")
if present?(result), do: log("#{result.severity}: #{result.title} in #{result.file}")

Replay and resume#

A workflow run keeps a journal of every host read, every agent result and every answer you gave. When a paused or interrupted run resumes, the program runs again from the top, but every recorded step returns its recorded value at once, so only the unfinished part really runs. This is why a workflow must not read the clock or random numbers directly: a replay would see different values and no longer match its journal. host(:now) and budget() are recorded, so they are safe.

Worked example: review-changes#

The built-in review-changes workflow in outline:

elixir
meta = %{
  name: "review-changes",
  description: "Review the uncommitted changes from several angles and keep only findings that survive an adversarial check",
  phases: ["Review", "Verify", "Report"],
  budget: 48,
  args: %{
    base: %{type: :string, default: "", doc: "Git ref to diff against (empty = working tree vs HEAD)"},
    dimensions: %{type: :list, default: ["correctness", "security", "performance", "maintainability"], doc: "Review lenses, one agent each"}
  }
}

phase("Review")
diff = host(:git_diff, base: args.base)
if String.trim(diff) == "", do: complete(%{summary: "No changes to review.", confirmed: []})
files = host(:changed_files, base: args.base)

reviews =
  panel(args.dimensions, fn dim ->
    agent("You review code changes for #{dim} problems only. …",
      schema: findings_schema, capability: :read_only, name: "review:#{dim}")
  end)

if Enum.all?(reviews, &is_nil/1), do: pause(:no_progress, "Every reviewer failed")

phase("Verify")
# one skeptical agent per finding tries to refute it; only findings with quoted evidence survive

phase("Report")
path = write_report("review.md", report)
complete(%{summary: "… see #{path}", confirmed: confirmed, report: path})

What it shows:

  • Arguments with defaults: /workflow review-changes base=main overrides base; dimensions keeps its four lenses.
  • An early exit: with nothing to review, complete/1 ends the run successfully at once.
  • A panel: four reviewers run at the same time, one per lens, all read-only.
  • A deliberate pause: if every reviewer failed, the run stops with a reason instead of reporting "no problems".
  • A report: write_report/2 saves review.md in the run's folder and the result points to it.

The full source is in the library: open Workflows, select review-changes and scroll to Source, or Duplicate to project to get an editable copy.