All guides
Personas
A persona is one agent, declared in one file: .nxs-personas/<handle>.yaml. nxc reads that
folder; nothing but you writes it. There is no register verb, no built-in personas, and no way to
conjure one at runtime — which is the point. A declared agent is one you can read, diff, review and
commit, and once committed it is still there tomorrow. Commit it — see
the folder belongs in version control, which is not
housekeeping advice.
The minimum is two lines:
handle: coder
system_prompt: |
You are the coder. Do what the trigger message asks, then answer on your own thread.Everything else has a default that means what it meant before the field existed, so a declaration never breaks by standing still.
Who it is
handle: coder
job_title: Coder
job_description: Implements a work order on a branch and merges it.
expected_output: A short report of what changed, and the branch it is on.handle is the address — what you type after send --to. By convention it is also the filename
stem, though the YAML field is the source of truth.
job_title and job_description are not decoration. They are what nxc list shows a human
deciding whom to address, and what the persona itself is told at session start. A handle with no
description is a name in a directory that nobody can choose from — the one-liner does the work a
skill's frontmatter does, and it is worth a sentence of thought.
expected_output is the shape of the answer you want back. It reaches the persona's own identity
block, so it is the cheapest way to make several agents' replies comparable.
A handle may not start with __. That prefix is reserved for the engine's own identities (a
channel's supervisor is __channel__), which is what makes those unforgeable by a declaration.
What it is for
system_prompt is the persona's job, in your own words, and it is the last thing the model reads
before the conversation itself. The composed prompt is layered, in this order:
- The prime block — what
nxs prime --persona <handle>composes, and you can print it and read it yourself. It is the suite's session start, assembled in module order: the board (nxf prime), the project's memories (nxm prime), then chat's own block — this persona's identity (title, job, expected output, seniority, how it is reachable), whom it may address and what for, thenxccommand reference, and its answering rules. It goes first so that "who am I and how do I answer" anchors everything after it. - The project's
CLAUDE.md, when the persona'sclaude_md:policy asks for it. - The persona's own
system_prompt— closest to the actual conversation.
The answering rules in layer 1 are worth reading literally, because there are exactly two of
them and no third: answer in the thread you were handed, or say — in that same thread — that you
cannot. Both are nxc reply --thread <id>, and both end your turn. There is no way to say what
you are missing and carry on: you reply, your turn ends, and when the answer comes you are resumed
with everything you already know. send --to starts something new with somebody else; nxc list
shows who is there.
Three switches shape those layers:
prime: true # default. false opts out of layer 1 for a narrow role whose prompt covers it
claude_md: inherit # default | ignore (do not show project conventions) | override (reserved)
base_prompt: claude_code # default. `none` runs without the Claude Code preset underneathprime: also takes a map, when a persona should be given some of the suite and not the rest:
prime:
flow: false # no board for THIS persona — a pure reviewer does not need one
memory: true # the project's memories (the default)
chat: true # its identity, address book and answering rules (the default)Anything you leave out stays on, so the map above says one thing and changes one thing. The filter is per persona: excluding the board here removes that section from this persona's block and from no other.
memory: falsecan cost more than memories. If this project has runnxm migrate, its conventions have moved OUT ofCLAUDE.mdand into the store — that is what the migration does — sonxm primeis where the project's rules now come from, andclaude_md: inheritcontributes nothing. A persona that excludes the memories in such a project has the rules from nowhere. The engine cannot tell a migrated project from an unmigrated one, so it does not refuse the declaration; it tells the session instead, in one line of its own prompt, that it has neither and should runnxm primebefore changing anything it is unsure of.
Because layer 1 already teaches the answering loop, a trigger message can carry only the task. That is the whole reason the field defaults to on.
One sentence is not yours to switch off. Whenever the trigger that starts a session declared
that a thread is waiting on its reply, the composed prompt carries the forced ending — and it
carries it even under prime: false:
Obligation: thread <id> is waiting on your reply, and your turn may not end without one.
There are exactly two ways to end it:
- `nxc reply --thread <id> "<your result>"` — you are finished.
- `nxc reply --thread <id> --escalate "<what is in the way>"` — you cannot reach the result
and need help or a decision.Those forms are the whole set, and "I will wait and answer later" is not one of them. The rule sits in the engine rather than in your declaration on purpose — a step of a declared flow ends when its session answers, so a declaration that forgot to say it would leave the flow with nothing to read.
A step that can send work back is told about a third form, and only such a step: where the
channel declares on_needs_rework: for it (nxc guide channels), the same paragraph offers
nxc reply --thread <id> --needs-rework "<what must be put right>". The offer hangs on the forced
ending rather than on the usage block for the reason above — a role with prime: false is still
obliged to answer, and an obligation whose means nobody supplies is the defect this rule exists to
prevent.
What it runs as
stage: senior # junior | senior | principal
model: opus # fable | opus | sonnet — beats `stage` when both are given
tools: [Bash, Read, Write]
permissions: acceptEditsstage is a vocabulary that is not a model name. You decide how much thinking a job is worth
without knowing which models exist this month: junior → Sonnet, senior → Opus, principal →
Fable. One table joins the two, so the declaration's spelling and the engine's choice cannot drift
apart. Reach for model: only when you mean something the band cannot express — and note the
precedence is deliberate: the band is the coarse, discussable choice, a named model is you
overriding it.
tools has three states, and the difference matters. Omit it entirely and the agent runtime's own
full default toolset applies. Write tools: [] and the persona explicitly has none — a narrow,
non-agentic role. Write a list and it gets exactly that list. An omitted key and an empty list are
not the same thing.
A persona that will run nxc at all needs Bash, since that is how it answers — and where the
engine orders an answer, it grants that itself. Every commission tells the persona, in its own
system prompt, to end its turn with nxc reply --thread <id>, so whoever imposes that obligation has
to make sure it can be carried out: the trigger adds Bash to what the session may run, on top of
whatever you declared, without narrowing the toolset an omitted tools: gives it. You still declare
Bash for a persona that needs the shell for its work; what you no longer have to remember is that
answering is itself work.
Who may address it
addressable: general # the default: anyone may open a conversation directlyaddressable: [review] # only through these channels; a direct message is refusedThis is a property of the target, not a limit derived from the caller. That is what makes it enforceable: a channel-only persona refuses a direct message from everyone alike — human or agent, named or anonymous. A restriction derived from who you claim to be would mean that dropping your identity granted more, which is worse than no restriction at all.
Declaring it also keeps the persona out of nxc list's human rendering as an entry of its own,
which is usually what you want: if the way to reach four reviewers is to address the round they are
in, listing them individually invites exactly the call you do not want.
Who it may address
address_book:
- to: review
why: to get a change judged before merging
- to: pm
why: to report that a work order is doneThe why is the load-bearing half — it works like a skill's one-line description, a sentence a
model chooses from, which is why it is rendered everywhere the target is.
It has three states, exactly like tools:, and the difference matters.
# key omitted -> "nobody wrote this down": the persona is shown the whole declared team
address_book: [] # -> "commissions nothing": the persona is shown nobody at all
address_book: [{to: pm}] # -> that book, in the author's orderaddress_book: [] is the declaration for a role at a leaf of the tree: a pure reviewer, a
summarizer, anything whose only outbound call is the reply on its own thread. Saying it in the file
is the point — before this, the alternative was a sentence in the system_prompt ("you commission
nothing; ignore the list"), which is a prompt argued against a declaration.
An omitted key stays "not written down" and shows the whole team, so every persona declared before this existed means exactly what it always meant.
One thing the derived list leaves out: the channels the persona is a member of. Offering a
reviewer the review round it sits in is help in no reading, and commissioning your own channel is
not declaration cyclicity — that is refused when the catalogue loads — so nothing else would catch
it. A book you write yourself is honoured entry for entry, that channel included: the file is the
authority.
Note what the address book deliberately does not do in this milestone: it does not restrict. It is guidance a persona reads, for the same reason as above — deriving a limit from a droppable identity would be worse than useless.
What it needs to work
working_tree: exclusive # shared (default) | exclusiveexclusive says this persona's sessions need sole use of the repository's working copy and build
directory. A second chain that would collide waits instead. It is declared rather than inferred
because a persona may be named anything and the runtime cannot guess from a task that a checkout is
about to be branched. The full mechanics — what a claim covers, what releases it, and what it does
not protect you from — are in limits-and-safety.
A worked example
handle: coder
job_title: Coder
job_description: Implements a work order on a branch and merges it.
working_tree: exclusive
system_prompt: |
You are the coder. You are the first step of the declared `build-and-ship` channel, which means
somebody sent a work order to that channel and its supervisor opened a thread for you with the
work on it.
## How you finish — read this before you do anything else
You finish by ANSWERING ON YOUR OWN THREAD:
`nxc reply --thread <the thread id in your trigger message> "<what you did>"`.
That reply is what moves the operation on: the channel's supervisor is waiting for it, and it
starts the next step the moment it lands. There is no second call and nothing else to remember.
If you end your session without replying, the operation stands still until the channel's declared
`timeout` strikes.
If you genuinely cannot carry the task out, say so with `nxc reply --escalate "<why>"` on the same
thread. That is the ONE other thing you may say, and it is a declared signal the supervisor
branches on — not a way to hand back an unfinished task quietly.
tools: [Bash, Read, Write]
permissions: acceptEditsTwo things in that prompt are worth copying into your own. It states the finishing move first,
before the work, because a session that does the work and forgets to answer has achieved nothing
anybody can see. And it names escalation as a declared signal rather than a wastebasket — an agent
that is told "say --escalate when you cannot" will not invent a phrase for it that nothing can
read.
Declared but not yet read
Trustworthy documentation says which fields are inert. These parse, round-trip and validate, and nothing in the engine acts on them today:
session: fresh | continue— inert because the persona is not what decides.nxc send --to <persona>mints a fresh session; anxc reply --threadinto that persona's own thread resumes the session it already has, with everything it already knows; and inside a channel that declaressteps:, the step being entered decides for itself with its ownresume:(nxc guide channels). A policy on the persona has nothing left to decide that the verb or the step has not decided already.sub_agents: true | false.reports_to: <handle>.
Declaring them costs nothing and documents intent; do not build a process on them behaving.
claude_md: override is the one to be careful with, because it is only HALF unread. The
per-persona replacement document it names is not specified yet, so nothing composes one — but the
tag is not idle while it waits: it is simply not inherit, so a persona that declares it is
composed WITHOUT the project's CLAUDE.md, exactly as ignore would be. Declare ignore when that
is what you mean. inherit (the default) and ignore are both live.
The folder belongs in version control
.nxs-personas/ is not documentation of how your agents work — it is how they work, read fresh at
every spawn. And it lives in the same working copy the agents themselves are told to work in. That
is a feedback loop no other configuration in this system has: a git switch, a git stash or a
git checkout -- . by one persona changes how the next persona thinks.
So an uncommitted edit to a declaration is not really an edit at all. It is a change one branch holds and every other branch does not, in a folder whose contents decide the rules — and the thing that reverts it is ordinary, correct branch hygiene by somebody who has no way to know your file mattered. That is not a hypothesis: it is what happened here. Three declarations were rolled back by exactly that route, one of them a rule telling a persona to end every turn with an answer; the persona then ran without it and the runtime answered in its place on four threads before anyone noticed. It was found by hand, by comparing a stored session spec against the file on disk.
Commit the folder, and review changes to it the way you review code. Two things help you notice when it slipped anyway, and neither is a lock — the files stay editable at every moment, because what has to be stable is a running operation, not the directory:
- Every spawned session records the version of the declaration its prompt was built from, as
declarationHashin.nxs/agent-logs/<session>.spec.json. "Did this session run under the rule I wrote?" is a comparison, not a text search through a prompt. - When a persona is started under a different declaration than the last time it ran, the receipt of
the call that started it says so — a
declaration_changedentry inwarnings, naming both versions. It is a warning and not a refusal: changing a persona and then addressing it is the ordinary way to work, and the exit code stays0. A change is usually intended. An unnoticed one never is.
Both of those watch the persona's own file, and nothing else in the folder. A channels.yaml
that was rolled back the same way — different members, a different working_tree:, a different
timeout: — steers your agents just as much and is not reported by either. So the advice above
is not "the tooling has your back": it is version control that has your back, and this is a second
pair of eyes on the half of the folder it can see.
When a declaration is wrong
A malformed file is a loud validation error naming the path — never a silently skipped persona. A
missing folder is not an error: a workspace with no declarations resolves cleanly and simply has
nobody to address, and nxc list and nxs prime say so in as many words.
Referential problems — a channel naming a persona that does not exist, a persona declaring itself
reachable through a channel it is not a member of — are surfaced in nxs prime, and only in an
interactive context. A spawned persona is not shown its author's mistakes; a human at a keyboard is.
Next
- channels — putting several personas to work together.
- commands — addressing what you just declared.
- limits-and-safety — the caps around a running persona.