Repository navigation
Settings and Templating
Two ways to stop repeating yourself in a config, for two different problems.
${settings.NAME} |
{{ .fact }} |
|
|---|---|---|
| when | at load, once | per dispatch |
| where | anywhere in the config, every imported file | scope allowlist entries |
| for | a value that repeats — a channel, an org, a repo glob | a value that depends on the event |
settings:
review_channel: "#code-reviews"
org: acme
deploy_repo: "${settings.org}/deploys" # settings chain
bot_channel: "${env.BOT_CHANNEL}" # from the environment
policy:
agent_authored:
verbs:
slack.post: { channel: ["${settings.review_channel}"] }
gh.submit_review: { repo: ["${settings.deploy_repo}", "{{.owner}}/docs"] }A top-level settings: block is a map of names to values. Every
${settings.NAME} in the config is replaced by that value before the config
is parsed, so it works in any field — an allowlist entry, a channel, a repo
glob, a prompt, a host name. There is no list of "fields that support
settings".
It also reaches every imported file, in both directions: a setting declared
in conductor.yaml lands in triggers/review.yaml, and one declared in an
import lands in the root file. Split your config however you like; the settings
still apply.
A value can be:
settings:
plain: "#code-reviews" # a literal
fromenv: "${env.REVIEW_CHANNEL}" # the environment — conductor.env included
built: "${settings.org}/api" # another setting${env.NAME} reads the process environment, which is where conductor.env
ends up: park a value there and the setting picks it up with no other wiring.
A parameter that silently becomes empty turns an allowlist entry into one that matches nothing, and you find out during an incident. So each of these is a load error, named:
-
${settings.revue_channel}when you declaredreview_channel— an undeclared reference (one inside a#comment is not a reference); - a setting that resolves to an empty value;
-
${env.X}whereXis not set.
Unrelated syntax is untouched: the loader's own ${VAR} environment expansion
and a step's shell ${VAR} have no settings. prefix, so they keep their
meanings.
A setting supplies a value, never structure. Substitution happens inside
the parsed document rather than in its text, so a value containing newlines,
quotes, # or key: lands as exactly that string — it cannot close a scalar
and open a key you did not write. That matters most for a value you did not
author yourself: a pack's default, or a variable from a shared environment.
Packs have had this all along (Packs) — a pack declares
settings:and you supply the values. This is the same mechanism for your own config.
A Policy entry that contains {{ is rendered against the
event being dispatched, then matched as usual:
skill:
verbs:
slack.post: { channel: ["#pr-{{.number}}"] } # this PR's channel
policy:
agent_authored:
verbs:
gh.submit_review: { repo: ["{{.owner}}/docs"] } # this org's docs repoOn PR 42 the first grant means #pr-42 and nothing else; on PR 99 it means
#pr-99. One line, instead of one line per PR — or a wildcard you didn't want
to write.
The available facts are a closed set: number, owner, name, repo,
kind. That is all of them, and the rule for the list is one question — can
the author of a pull request choose this value?
title, head_ref, author, labels, comment_body and the connector's
enriched event context are therefore not available: they are free text the
person who opened the PR writes, so an entry built from one could be forged by
naming a branch (or a PR title) after the resource you wanted. inputs is out
for the same reason — an input can carry event text verbatim. An entry
referencing any of them renders empty and matches nothing.
Entries with no {{ are not rendered at all. A rendered value is matched
literally: your glob intent belongs in the pattern you wrote (acme/*),
not in a value the event supplied.
If a webhook source builds its repo: from the request body
(repo: "{{.body.owner}}/{{.body.name}}"), the sender chooses it. Conductor
marks those dispatches and withholds the usual own-target trust: no implicit
own-repo, no own memory scope, and repo/owner/name/number render empty
in an allowlist entry. Scope them with explicit verbs.<verb>.repo entries —
conductor validate warns so this is not a surprise.
A scope allowlist is a security check, so the renderer is not the one steps use:
-
No secrets.
{{.secrets.x}}and{{.vaults…}}are not in scope, and a credential a connector published into its event context is redacted before the render sees it. A check that can read a secret can be made to leak it. -
No
kv, novault, no side-effecting functions — onlydefaultandcoalesce. An authorization check does not perform reads. - Nothing the agent supplied. The option value being checked is never a render source. The agent cannot influence what it is checked against.
- Fail-closed. A template that errors, names a fact this dispatch doesn't have, or renders to empty matches nothing. It never falls open.
- The value is the same for every event →
${settings.X}. It works everywhere in the config, not just in allowlists. - The value depends on the event →
{{ .fact }}, in the allowlist entry. - Both, in the same list, is fine:
channel: ["${settings.review_channel}", "#pr-{{.number}}"].
-
Policy —
verbs:and the rest ofpolicy.agent_authored -
Agent-Skill — per-verb grants (
skill.verbs) and their scoping -
Packs — a pack's own
settings:block -
Configuration —
imports:,conductor.env, and the load order
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)