-
Notifications
You must be signed in to change notification settings - Fork 0
Reuse
Real configs repeat themselves: steps share a model and a tone, and near-identical triggers repeat
the same filter:/steps: per team.
A step has two ways to reuse a chunk of config, and they differ in one thing — when they happen:
| what it does | when | |
|---|---|---|
<<: *base |
dumb override: the step's own key wins outright, scalars and lists alike | at parse, before conductor sees the file |
extends: *base |
field-aware merge: scalars override, lists append, maps deep-merge, guidance: stacks
|
at load, with both layers in hand |
That is the whole distinction. <<: is finished by the time conductor reads the document, so there
is no base left to append to; extends: hands conductor both layers, so it can be smarter. Reach
for <<: when you want a copy of a base, and extends: when you want to add to one.
Two more mechanisms sit alongside them: extends: on a map section or a trigger (a different
feature that shares the word — see below), and layered guidance: through the policy cascade.
Both forms take the same thing on the right: a YAML alias to an anchor, or an inline map. Anchors
must be defined somewhere, so park them under any top-level x- key — the loader ignores
x--prefixed sections (the docker-compose extension-field convention), and they exist purely to
hold anchors.
x-templates:
reviewer: &reviewer
type: agent
model: heavy
guidance: "Terse and human."
skill: { verbs: [github.comment] }This works the same in a Packs — a pack author gets both forms with no extra machinery, and the anchors stay inside the pack's own file.
triggers:
- on: gh.review_requested
steps:
- <<: *reviewer # merge the anchor…
id: fix # …then this step's own fields, which win
model: light # OVERRIDES
skill: { verbs: [github.submit_review] } # REPLACES — the whole map, not just verbs
prompt: "Fix the failing checks on {{.repo}}#{{.pr}}."Plain YAML semantics throughout: a key the step sets replaces the merged one whole. A list is not
appended, a map is not deep-merged, and a guidance: is not stacked.
workflows:
review:
steps:
- extends: *reviewer
id: fix
model: light # OVERRIDES (scalar)
guidance: "Also cite file:line." # APPENDS → "Terse and human." then this
skill: { verbs: [linear.create] } # APPENDS → [github.comment, linear.create]
prompt: "Fix the failing checks on {{.repo}}#{{.pr}}."-
scalars (
model,workspace,mode, …) — the step overrides. -
lists (
skill.verbs,network, …) — append, base items first. -
maps (
labels,env,skill, …) — deep-merge, recursively. -
guidance:— always appends, base tone underneath. There is no opt-in: a base that set the house voice should still be heard under a step that adds to it.
Chains work, resolved base-first, so the outermost layer speaks last:
x-templates:
house: &house { type: agent, guidance: "House voice." }
team: &team { extends: *house, guidance: "Team voice." }
# a step with `extends: *team` and its own guidance gets all three, in that orderAn extends: chain is depth-bounded. An alias cannot loop (YAML requires the anchor first), but a
hand-written chain of inline maps could, and that is a load error rather than a hang.
Always-appending is only safe if there is a way out. Both are YAML tags on the step's own value, read by conductor during the merge:
- extends: *reviewer
guidance: !override "Only this." # replace instead of appending
- extends: *reviewer
guidance: !reset # drop the inherited value entirely
skill: { verbs: !reset } # works on any inherited list, map, or scalar!reset drops both layers for that key; !override takes the step's value verbatim. A tag on a
field nothing was inherited for means the same thing either way, so it is simply consumed.
The older
guidance: { replace: … }form means exactlyguidance: !override …and keeps working. The tag is the spelling to reach for — it works on every field, not just guidance.
Unusual, but defined: <<: resolves first (it is YAML-level and already finished), then extends:
merges field-aware on top of that result. So a guidance: that arrived through <<: counts as the
step's own, and the extends: base stacks underneath it.
-
It does not touch identity. A step's identity is still
name:→ structural (<workflow>/<id-or-index>) → fingerprint. Extending a base contributes nothing to it; if the base happens to setname:, that merges like any other field and its consequences are the ordinary ones (see Steps). -
It is not a reference. Both forms copy config at load; neither leaves anything to point at.
A
team:role and a pack overlay address a step where it lives —<workflow>/<step-id>, or<workflow>[<n>]for a step with noid:. -
Neither crosses
imports:. An anchor defined inconfig.yamlis invisible to an importedconf.d/*.yaml— that is YAML, not a conductor limit. For cross-file reuse of a whole section, use the map-sectionextends:below.
A different feature that shares the word. A named entry declares extends: <name> to inherit from
another entry in the same section. Supported on runtimes:, workflows:, handoffs:, and
triggers: — never on a step, whose extends: takes an anchor rather than a sibling key.
Resolution runs once at load, after imports: merge and before validation, which is what makes
it work across files where an anchor cannot.
runtimes:
remote:
use: cli
host: build-box
isolation: { mode: namespace }
remote-codex:
extends: remote # inherits host/isolation
command: [codex] # slices: the child replaces| Field kind | Rule |
|---|---|
scalars (provider, model, host, …) |
child wins when set; otherwise inherited |
pointers / blocks (isolation, session, policy, …) |
child wins when present; otherwise inherited |
maps (labels, env, inputs, trigger options) |
deep-merged per key — child keys override, the parent's other keys are kept |
slices (command, trigger steps/hooks) |
child replaces when it sets a non-empty value; otherwise inherited |
trigger filter
|
child replaces when set; otherwise inherited. Its shape is its boolean structure, so there is no key-wise merge to do |
guidance |
stacks (parent parts, then child parts) — see below |
Chains are allowed (c → b → a, resolved root→leaf). A cycle or an unknown extends:
target is a load error. One documented limitation: a plain bool field (e.g. a step's
archive_when_done, a runtime's default) is inherited only when the child leaves it false — a
child can't force a parent's true back to false. The optional fields that matter are pointers or
strings, so this rarely bites.
Not yet on
connectors:,stores:,vaults:. These decode through a retained raw YAML node (their type-specific sub-blocks aren't plain struct fields), which needs a different merge, and they rarely duplicate in practice.extends:there may come later.
Triggers reference a parent by its name:. Options deep-merge; filter, steps and hooks replace;
policy/gate/group fill if unset. A base that exists only to be extended is marked
abstract: true: it never fires and is stripped after resolution, so it needs no on:.
triggers:
- name: review-base # a base, not a live trigger
abstract: true
filter: { not_draft: true }
steps:
- { id: r, type: agent, name: reviewer, prompt: "Review {{.repo}}#{{.pr}}." }
- on: gh.review_requested
extends: review-base
filter: { repo: [org/api], not_draft: true } # replaces the base's — restate what you keep
- on: gh.review_requested
extends: review-base # no filter of its own: inherits the base'sBoth children inherit the base's steps. A child that writes its own filter: owns it whole — the
shape of a filter is its boolean structure, so a key-wise merge would have no defined meaning; a
child that writes none takes the base's as-is. The base is gone from the running config. Trigger extends: resolves before the multi-source on: list
expansion, so a child may also inherit a base's on:. An abstract: true base cannot be
on: manual (it is never a conductor run target).
Guidance (house tone/format appended to an agent's prompt) is additive — a stack, not a value that the most specific level overwrites. It is also entirely config-driven: conductor ships no tone of its own. If you configure nothing, nothing is injected. From the bottom up:
-
Layer 0 — the scoped baseline: [[Policy|
policy.guidance]]. Because policy cascades global → connector → trigger, the baseline is scopable. Scopes stack by default (a trigger's guidance adds under the global tone); a scope that uses the{ replace: … }form resets the cascade from that scope down. -
The step's own
guidance— including everything anextends:chain stacked underneath it — sits on top of the baseline.
config.example.yaml ships a reasonable house tone under policy.guidance you can adopt or
change — it is an example, not a default conductor imposes.
guidance: accepts three forms, at both the policy scope and the step:
guidance: "one block" # a single part
guidance: [ "first", "second" ] # several parts, in order
guidance: !override "only me" # reset: drop everything below this level, use only this
guidance: { replace: "only me" } # the older spelling of the same thing-
guidance: ""or[]at a level contributes nothing but does not suppress the levels below. -
guidance: { replace: "" }disables guidance entirely for that agent. - A step's
extends:, a sectionextends:child, and ateam:role filled in from the step it references all inherit the base's guidance underneath their own, unless they escape with!override(or the older{ replace }) or drop it with!reset. A<<:anchor does not stack: a mergedguidance:is replaced wholesale by the step's own, because that is what a YAML merge does.
agent_guidance: (the old top-level field) still works — it is folded into the global
policy.guidance for back-compat, so connector/trigger-scoped guidance stacks on top of it. If both
are set, policy.guidance wins. conductor config migrate (and the boot auto-migration) rewrites a
top-level agent_guidance: to policy.guidance so configs converge on the canonical form; the alias
stays accepted, so migrating is optional.
Behavior change (v0.7.4): a per-agent
guidance:now appends to the baseline instead of replacing it. To restore the old replace-the-global behavior, writeguidance: { replace: … }.
policy:
guidance: "Write like a busy engineer: a sentence or two, plain and direct."
connectors:
gh:
use: github
policy:
guidance: "On PRs, lead with the point and propose a concrete fix." # stacks under global
workflows:
review:
steps:
- id: reviewer
type: agent
guidance: "Flag only what a thoughtful senior would bother raising." # stacks on top
prompt: "Review {{.repo}}#{{.pr}}."The reviewer step reached from a gh trigger sees all three blocks; the same step on a Slack
trigger sees only the global tone plus its own.
- Steps — step behavior, step references, and step identity
-
Policy — the cascade
policy.guidancerides on -
Runtimes, Workflows — the sections that support the map-section
extends: - Configuration — the full trigger grammar
Setup
The model
- Connectors
- Workflows
- Reuse
- Settings-and-Templating
- Packs
- Verbs
- Code-Steps
- Stores
- Runtimes
- Model-Selection
- Model-Discovery
- Steps
- Decide-Steps
- Grouping
- Memory
- Binary-Data
- Agent-Skill
- Policy
- Gates
- Teams
- Outcomes
- Cost-Accounting
- Secrets
- Hosts
- Isolation
- Trust-and-Isolation
Connectors
Operations
- One-Shot
- Callable-Service
- Runs
- Hand-offs
- Notifications
- Migration
- Controllers (legacy name → Runtimes)