Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ make validate-examples # validate all examples
- **executor/**: Agent execution
- `agent.py` - `AgentExecutor` handles prompt rendering, tool resolution, and output validation for single agents
- `script.py` - `ScriptExecutor` runs shell commands as workflow steps, capturing stdout/stderr/exit_code
- `set_step.py` - `SetExecutor` evaluates Jinja2 expressions for `type: set` steps and binds typed values into the workflow context (no LLM, no subprocess). Supports single `value:` and multi `values:` forms with auto / explicit `output_type:` coercion.
- `template.py` - Jinja2 template rendering
- `output.py` - JSON output parsing and schema validation

Expand Down Expand Up @@ -113,19 +114,21 @@ make validate-examples # validate all examples

1. CLI parses YAML via `config/loader.py` → `WorkflowConfig`
2. `WorkflowEngine` initializes with config and provider
3. Engine loops: find agent/parallel/for-each/script → execute → evaluate routes → next
3. Engine loops: find agent/parallel/for-each/script/set → execute → evaluate routes → next
4. Parallel groups execute agents concurrently with context isolation (deep copy snapshot)
5. For-each groups resolve source arrays at runtime, inject loop variables (`{{ item }}`, `{{ _index }}`, `{{ _key }}`)
6. Script steps run shell commands via asyncio subprocess, expose stdout/stderr/exit_code to context
7. Routes evaluated via `Router` using Jinja2 or simpleeval expressions
8. Final output built from templates in `output:` section
7. Set steps render Jinja2 expressions and bind typed values to context (no LLM, no subprocess) via the shared `WorkflowEngine._run_set_step` helper, which enforces `output:` schema in all three positions (main loop, parallel group, for-each iteration) and emits `set_started` / `set_completed` / `set_failed`
8. Routes evaluated via `Router` using Jinja2 or simpleeval expressions
9. Final output built from templates in `output:` section

### Key Patterns

- **Context modes**: `accumulate` (all prior outputs), `last_only` (previous only), `explicit` (only declared inputs)
- **Failure modes** for parallel/for-each: `fail_fast`, `continue_on_error`, `all_or_nothing`
- **Route evaluation**: First matching `when` condition wins; no `when` = always matches
- **Tool resolution**: `null` = all workflow tools, `[]` = none, `[list]` = subset
- **Set step typing**: `output_type` defaults to `auto` (safe YAML parse with `_to_json_safe` normalisation — `datetime`/`date`/`time` → ISO 8601, non-string dict keys and other non-JSON-safe values raise `ExecutionError`). Explicit `string`/`number`/`integer`/`boolean`/`list`/`dict` only valid on single `value:`. `WorkflowContext.store` accepts any JSON-safe value (scalars/lists from `set` steps in addition to the dicts produced by LLM / script / gate / parallel-group outputs); `_add_agent_input` returns the scalar verbatim for `step.output` and raises a clear `KeyError` for `step.output.field` shorthand on non-dict outputs.
- **Reasoning effort**: `runtime.default_reasoning_effort` sets a workflow-wide default; per-agent `reasoning.effort` overrides it. Allowed values: `low`, `medium`, `high`, `xhigh`. Each provider translates the unified value to its native API (Copilot: `reasoning_effort` on the session, validated against the model's `supported_reasoning_efforts`; Claude: extended thinking with budget mapping low=2048, medium=8192, high=16384, xhigh=32768 tokens, with `temperature` coerced to 1.0 and `max_tokens` bumped to fit the budget). See `examples/reasoning-effort.yaml`.

### Debugging `--web-bg` failures
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Conductor makes multi-agent workflows — code review pipelines, research-then-s
- **Parallel execution** - Run agents concurrently (static groups or dynamic for-each)
- **Sub-workflow composition** - Reusable sub-workflows with templated `input_mapping`, usable inside `for_each` groups for dynamic fan-out
- **Script steps** - Run shell commands and route on exit code or parsed JSON stdout
- **Set steps** - Bind one or more Jinja2-evaluated values into the context (no LLM, no subprocess) for derived flags, computed defaults, and constants reused by many later prompts
- **Dialog mode** - Agents can pause for multi-turn conversation when uncertain
- **Reasoning effort** - Unified `reasoning.effort` (low/medium/high/xhigh) per agent or workflow-wide, translated to each provider's native API
- **Workspace instructions** - Auto-discover and inject `AGENTS.md` / `CLAUDE.md` / `.github/copilot-instructions.md` into every agent's prompt
Expand Down Expand Up @@ -300,6 +301,7 @@ See the [`examples/`](./examples/) directory for complete workflows:
| [parallel-research.yaml](./examples/parallel-research.yaml) | Static parallel execution |
| [design-review.yaml](./examples/design-review.yaml) | Human gate with loop pattern |
| [script-step.yaml](./examples/script-step.yaml) | Script step with exit_code routing |
| [set-step.yaml](./examples/set-step.yaml) | Set step deriving named values + boolean-routed branching |

**More examples and running instructions:** [examples/README.md](./examples/README.md)

Expand Down
87 changes: 87 additions & 0 deletions docs/workflow-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,93 @@ routes:

**Environment variable note** — values in `env` are passed as-is to the subprocess (they are not rendered as Jinja2 templates). Use `${VAR}` syntax in the workflow YAML loader if you need environment variable substitution in env values.

### Set Steps

Set steps evaluate one or more Jinja2 expressions and bind the typed results into the workflow context. No LLM call, no subprocess, no I/O — they're pure context transformations. Use them to combine inputs, derive flags from prior outputs, compute defaults, or normalise a value once for many downstream prompts to share.

```yaml
agents:
# Single binding — output is the typed scalar / list / dict.
- name: compute_slug
type: set
value: "{{ workflow.input.org }}/{{ workflow.input.repo }}"
# accessible as: compute_slug.output (a string)
routes:
- to: derive_flags

# Multi-binding — output is a dict, accessible as step.output.<key>.
- name: derive_flags
type: set
values:
is_breaking: "{{ research.output.severity in ['high', 'critical'] }}"
target_branch: "{{ workflow.input.branch or 'main' }}"
effective_model: "{{ workflow.input.model or 'claude-sonnet-4-5' }}"
routes:
- to: breaking_path
when: "{{ output.is_breaking }}"
- to: safe_path
```

Exactly one of `value:` or `values:` must be present.

**Type detection** — by default, the rendered string is parsed with safe YAML (equivalent to `yaml.safe_load`); booleans, numbers, lists, and dicts are returned as native types. Parse failures and pure-comment renders fall back to the raw string. Empty / whitespace-only renders become `""`, not `None`. `yaml.safe_load` produces `datetime`/`date`/`time` objects from strings like `"2024-01-02"`; these are converted to their ISO 8601 string form so checkpoint round-trips and dashboard payloads stay JSON-safe. Any other non-JSON-safe Python value raises `ExecutionError`.

**Explicit `output_type:`** (single `value:` only) forces a specific coercion:

| Value | Behaviour |
|-------|-----------|
| `auto` (default) | YAML safe-load with the rules above |
| `string` | Keep the raw rendered string verbatim |
| `number` | Try `int` then `float`; raise on failure |
| `integer` | `int`; raise on failure |
| `boolean` | Case-insensitive `true`/`false`/`1`/`0`/`yes`/`no`/`y`/`n`/`on`/`off` |
| `list` | Parse via YAML; assert the result is a list |
| `dict` | Parse via YAML; assert the result is a dict |

Per-key typing on multi `values:` is not supported.

**Multi-binding ordering** — every binding in a single `values:` step renders against the *original* pre-step context. Later bindings cannot reference earlier ones in the same step. If you need ordered dependencies, chain multiple set steps:

```yaml
- name: step_a
type: set
value: "{{ workflow.input.x | upper }}"
- name: step_b
type: set
value: "{{ step_a.output }}-suffix"
```

**Routing on set output** — routes attached to a set step evaluate against the bound value directly. Dict outputs expose `{{ output.<key> }}` (Jinja2) and bare `<key>` (simpleeval); scalar / list outputs expose only `{{ output }}`:

```yaml
# Multi-values step — route on a derived dict field.
- name: derive_flags
type: set
values:
is_breaking: "{{ severity == 'high' }}"
routes:
- to: breaking_path
when: "{{ output.is_breaking }}"
- to: safe_path

# Single-value step — route on the scalar itself.
- name: flag
type: set
value: "{{ workflow.input.severity == 'high' }}"
routes:
- to: hi
when: "{{ output }}"
- to: lo
```

**Optional output schema** — set steps support the same `output:` schema as LLM and script agents, but only when the rendered value is a dict (which is always the case for multi `values:`, and may be the case for single `value:`). If a single-`value:` step declares `output:` but produces a scalar / list, the engine raises a friendly `ValidationError` pointing to `values:` as the intended shape.

**Composition** — set steps are allowed inside `parallel` groups (each member publishes its bound value to context) and as the inline agent of a `for_each` group (one bound value per item). Inside a parallel group, set templates cannot reference sibling group members (the validator catches this at config time, since the engine renders against a pre-group snapshot).

**Restrictions** — set agents cannot have `prompt`, `model`, `provider`, `tools`, `system_prompt`, `command`, `args`, `env`, `working_dir`, `timeout`, `workflow`, `options`, `input_mapping`, `max_depth`, `retry`, `dialog`, `reasoning`, `timeout_seconds`, `max_session_seconds`, or `max_agent_iterations`. They count toward `limits.max_iterations` like any other step.

**Events** — set steps emit `set_started` / `set_completed` / `set_failed` (mirroring the script-step lifecycle) in all three positions: linear main loop, parallel group member, and for-each iteration. The `set_completed` payload carries `output_type`, `output_keys` (sorted, empty for scalars), and `value_repr` (a JSON-safe preview, truncated at 512 chars).

### Sub-Workflow Steps

Sub-workflow steps reference external workflow YAML files, enabling composable and reusable workflow building blocks. The sub-workflow runs as a black box — its internal agents are not visible to the parent.
Expand Down
22 changes: 22 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ Parallel code validation checks. Demonstrates:
conductor run examples/parallel-validation.yaml --input code="def hello(): print('world')"
```

## Set Step Examples

### set-step.yaml

Derive named values from Jinja2 expressions without spending an LLM call. Demonstrates:

- Single `value:` binding (scalar output accessible as `step.output`)
- Multi `values:` binding (dict output accessible as `step.output.<key>`)
- Conditional routing on a derived boolean (`when: "{{ output.is_breaking }}"`)
- Combining derived values with downstream script-step consumers

```bash
# Breaking-change branch
conductor run examples/set-step.yaml \
--input org=microsoft --input repo=conductor --input severity=high

# Safe-change branch with a custom model
conductor run examples/set-step.yaml \
--input org=acme --input repo=widget --input severity=low \
--input model=claude-haiku-4.5
```

## Human-in-the-Loop Examples

### design-review.yaml
Expand Down
113 changes: 113 additions & 0 deletions examples/set-step.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Set Step Example
#
# Demonstrates `type: set` — a deterministic step that evaluates one or more
# Jinja2 expressions and binds the typed results into the workflow context.
# No LLM call, no subprocess; ideal for combining inputs, deriving flags,
# computing constants, or normalising values once for many downstream
# consumers.
#
# This workflow:
# 1. compute_slug — single `value:` step combining org + repo into a string.
# Accessible as compute_slug.output.
# 2. derive_flags — multi `values:` step deriving a boolean, a default
# branch, and a default model. Accessible as
# derive_flags.output.is_breaking, .target_branch,
# .effective_model.
# 3. A route on derive_flags branches on the boolean using
# `when: "{{ output.is_breaking }}"`. Routes attached directly to a set
# step evaluate against the step's bound value — dict outputs expose
# `{{ output.<key> }}`, scalar outputs expose `{{ output }}`.
# 4. The destination is a script step that simply echoes the chosen path,
# so the example runs without a provider connection.
#
# Try it:
# uv run conductor run examples/set-step.yaml \
# --input org=microsoft --input repo=conductor --input severity=high
# uv run conductor run examples/set-step.yaml \
# --input org=microsoft --input repo=conductor --input severity=low

workflow:
name: set-step-demo
description: "Demonstrates `type: set` for deriving named values from Jinja2"
version: "1.0.0"
entry_point: compute_slug

input:
org:
type: string
description: GitHub org
repo:
type: string
description: GitHub repo
severity:
type: string
description: Severity label used to derive the is_breaking flag
branch:
type: string
description: Optional branch override
required: false
model:
type: string
description: Optional model identifier
required: false

runtime:
provider: copilot

limits:
max_iterations: 10

agents:
# Single value: bind a single Jinja2 expression as a scalar.
- name: compute_slug
type: set
description: Combine org + repo into a single string accessible as compute_slug.output
value: "{{ workflow.input.org }}/{{ workflow.input.repo }}"
routes:
- to: derive_flags

# Multi values: render each key independently against the original context.
# Output is a dict accessible as derive_flags.output.<key>.
- name: derive_flags
type: set
description: Derive a boolean flag, default branch, and default model
values:
is_breaking: "{{ workflow.input.severity in ['high', 'critical'] }}"
target_branch: "{{ workflow.input.branch or 'main' }}"
effective_model: "{{ workflow.input.model | default('claude-sonnet-4-5') }}"
routes:
- to: breaking_path
when: "{{ output.is_breaking }}"
- to: safe_path

- name: breaking_path
type: script
description: Handler for breaking changes
command: python3
args:
- "-c"
- "print('BREAKING change on {{ derive_flags.output.target_branch }} for {{ compute_slug.output }}')"
routes:
- to: $end

- name: safe_path
type: script
description: Handler for non-breaking changes
command: python3
args:
- "-c"
- "print('safe change on {{ derive_flags.output.target_branch }} for {{ compute_slug.output }}')"
routes:
- to: $end

output:
slug: "{{ compute_slug.output }}"
is_breaking: "{{ derive_flags.output.is_breaking }}"
target_branch: "{{ derive_flags.output.target_branch }}"
effective_model: "{{ derive_flags.output.effective_model }}"
summary: |
{%- if breaking_path is defined -%}
{{ breaking_path.output.stdout | trim }}
{%- else -%}
{{ safe_path.output.stdout | trim }}
{%- endif -%}
1 change: 1 addition & 0 deletions plugins/conductor/skills/conductor/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ For runtime config, context modes, limits, and cost tracking, see [references/au
| `entry_point` | First agent/group to execute |
| `routes` | Where agent goes next (`$end` to finish, `self` to loop) |
| `type: script` | Shell command step (captures stdout, stderr, exit_code; JSON stdout is auto-merged) |
| `type: set` | Pure-context step that evaluates Jinja2 expressions and binds typed values (no LLM, no subprocess); supports single `value:` and multi `values:` |
| `type: workflow` | Sub-workflow agent — runs another YAML file as a black box (supports `input_mapping`, `max_depth`) |
| `parallel` | Static parallel groups (fixed agent list) |
| `for_each` | Dynamic parallel groups (runtime-determined array; supports `type: workflow` agents) |
Expand Down
Loading
Loading