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.
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#
| Key | Meaning |
|---|---|
name | the workflow's name; it also becomes a slash command |
description | one line shown in lists |
phases | the phase titles, in order, shown as the run's progress |
budget | the most agents one run may start (1 to 1,024); without it, the workflow agent budget setting applies (128 by default) |
max_live | how 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) |
args | named arguments, each with type, default and doc; the program reads them as args.<name> |
Functions#
| Function | What 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:andprovider:(by name) andeffort:("low","medium","high"or"max") to run this agent on something other than the workflow's default;isolation: :worktreeto give the agent its own copy of the project, andmax_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
Filemodule,System.cmd,System.shell,:os.cmd,Path.expand,Path.absname,Path.wildcard(usehostinstead); - anything that differs between two runs: clocks, random numbers, unique integers,
Process.sleep(usehost(: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:
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:
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=mainoverridesbase;dimensionskeeps its four lenses. - An early exit: with nothing to review,
complete/1ends 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/2savesreview.mdin 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.