A job description for an AI agent is a versioned document that states what one bot is for, what it may and may not do, who it answers to, when it runs, and what finished work looks like — read by the runtime as configuration and by a person as standing orders. It is the artefact that turns “there is an agent that does research” into something two people can disagree about precisely.
Klingbar labels it a role contract in the console. On disk it is a file named jd.md, and the name matters less than the property: it is one document, versioned, reviewed as a diff, and consulted by the scheduler before every run rather than pasted into a prompt when someone remembers.
This guide covers the two halves of the file, what belongs in each, a worked example, how to change one safely, and the four ways these documents rot.
A prompt is not a job description
A prompt is an instruction for one call. A job description is standing orders for an indefinite number of them, including the ones nobody is watching.
The practical difference shows up the first time a bot does something defensible but unwanted. With a prompt, the conversation is “what was it told that time” and the answer is unrecoverable, because the instruction was assembled at call time from templates, memory, and whatever the last person typed. With a job description, the conversation is “what does its contract say”, and the answer is a file with a history. You can diff it against last month, see who approved the change, and either fix the document or accept that the bot did what it was told.
That is also why the document is separate from an org chart written as code. The chart declares who exists, which unit they sit in, and what they may spend. The job description declares how one of them works. Keeping them apart means a restructure does not rewrite everyone’s standing orders, and a change to one bot’s duties does not touch the organization’s authority file.
The two halves
A usable job description has a machine half and a human half in the same file, and both are load-bearing.
The machine half is structured configuration: identity, placement, which engines the bot may use, what it is allowed to reach, and the schedule of duties it runs. The runtime reads it and refuses to start if it is wrong.
The human half is prose: what the bot is for, where it stops, who it works with, how it works, what done means, and when it escalates. Nothing parses this, and it is not decoration — it is the part a reviewer reads before approving a change, and the part the bot itself is handed as its standing context.
Splitting them into two files is a common instinct and a mistake. The configuration and the intent drift apart the moment they can be edited independently.
The machine half, annotated
Here is a complete frontmatter block, in TOML, fenced at the top of the file:
schema = "agency.jd.v1"
name = "scout"
title = "market analyst — competitive landscape"
status = "active"
unit = "research"
reports_to = "@dana"
authorities = ["kb-write"]
account = "scout"
protected = ["status", "account", "authorities", "unit", "reports_to"]
[engines]
chain = ["claude", "codex"]
safety_profile = "Default"
[engines.policy]
web = "deny"
net = "allow"
tools_deny = ["Bash"]
[[duties]]
name = "scan"
kind = "standard"
schedule = { cron = "0 14 * * *" }
model = "haiku"
effort = "low"
timeout = "15m"
stale = "6h"
prompt = "duties/scan.md"
[budgets.envelope]
usd_5h = 1.00
usd_24h = 3.00
cost_center = "research"
Four things in there deserve more attention than they usually get.
reports_to is an identity, not a label. “Someone should review this” is not enforceable. A named identity is what a gate can check, and it is what makes the approval trail attributable to a person rather than to a role nobody currently holds.
protected is a list of fields that cannot be waived. Status, account, placement, authorities, and reporting line are the fields whose quiet edit would change what a bot is allowed to be. Marking them protected means an update that touches them is refused rather than accepted with a warning nobody reads.
Duties carry a schedule, a ceiling, and a staleness bound. A duty that can run forever will eventually run forever. timeout bounds one execution; stale says how old a result may be before it stops counting as current. Both are cheap to write and expensive to add after an incident.
The engine policy is a deny list you can read. Whether this bot may reach the open web, which tools it may not call, which engine accounts it uses. If that lives in a platform setting somewhere instead of in the contract, a reviewer approving a duty change has no way to see the blast radius of what they are approving.
The human half: six sections
The prose body has six required headings, and a file missing one is refused rather than rendered with a gap. The requirement is deliberately mechanical, because these are exactly the sections writers skip.
What I do. Two or three sentences, in plain language, about the bot’s actual job. If it takes a paragraph to say, the role is probably two roles.
Boundaries. Written as refusals, not aspirations. “I never touch money, credentials, or production writes.” “I never act on another bot’s work — I hand it to their inbox.” “I never approve my own proposal.” A boundary phrased as a preference (“I try to avoid…”) is not a boundary, and both the reviewer and the bot will read it as one.
Counterparts. Who hands this bot work and who receives its output. This is the section that reveals accidental isolation and accidental dependency, and it is the reason to write it even for a bot that has no counterparts: stating “no one hands me work” is a design decision, and it should be visible as one.
How I work. The shape of one run. One tick, one duty; what it reads, what it produces, where the result is filed. A reader should be able to predict what a single execution looks like.
Definition of done. Borrowed from software practice, where a shared definition of done exists to stop “finished” from meaning something different to everyone in the room. For a bot it has to be testable from the outside: the greeting is posted and the outcome is filed. Anything that required a decision the bot does not hold is not done — it is escalation.
Escalation. The named conditions under which the bot stops and asks, and the identity it asks. Money, credentials, another bot’s work, and anything outside this document are the usual four. This section is what keeps “the bot did something surprising” from becoming a category of event.
Changing one safely
A job description is standing orders, so editing one is a governance action rather than a text edit, and the mechanics should reflect that.
In Klingbar an amendment reopens exactly one authored field or one prose section, and it carries the digest of the version being edited as a parameter. The first attempt is refused, and the refusal mints a named approval record; a person answers that record; the amendment is re-issued carrying the answer. Because the base digest sits inside the fingerprint the approval covers, a yes given to one version of an edit cannot be spent against a version that moved underneath it. The general shape is covered in human approval workflows; the specific reason it applies here is that a role contract is the one document whose quiet edit changes what a worker will do tomorrow without anyone watching it happen.
Authored changes are staged rather than live. Applying them is a separate, verifying step that refuses on a dirty contract, and the waivers it accepts are named and re-decided on the box rather than asserted by the browser. Protected fields have no waiver at all.
Four ways these rot
The kitchen-sink contract. Every incident adds a sentence, nobody removes one, and after a year the document is four pages that no reviewer reads in full. Prune it. An unread contract provides no control, only paperwork.
Secrets in the file. No key, token, or account password belongs in a job description, and the platform should make that structurally impossible rather than merely discouraged — see sealing agent credentials.
Aspirational language. “Be helpful”, “use good judgment”, “act professionally”. None of it constrains anything. Replace each with either a refusal or a definition of done.
Runtime state. What the bot is doing right now, when it last ran, how much it has spent this month. That belongs in the surface that reads it — see what an agent control plane is — not in the contract. A document that changes hourly cannot be reviewed, and a document nobody reviews is not a contract.
Write the shortest version that is true, make every boundary a refusal, name a human in the escalation section, and change it the way you would change a policy rather than the way you would change a prompt.