Skip to content

Intent manifest

<profile>/
intents/
intent.schema.json # optional, from `wloop profile schema`, for editors
<name>/
intent.toml # the definition
instructions.md.tmpl # the turn's instructions (Go text/template)
intent=<name>.md # judgment blocks, anywhere in the condition tree
  • The name is the directory’s: a lower-case letter, then 1 to 23 lower-case letters, digits or -. intent.toml has no name key.
  • A name may not be a CLI command, a built-in action id, a profile dimension, or one of try, eval, settings, intents and tags.
  • A directory named as a bundled intent overrides it file by file: each file it holds replaces the bundled file of the same name, and the bundled intent’s other files are kept. An intent.toml there replaces the bundled manifest whole; files whose name starts with . are ignored. When the result does not load, the bundled intent stays and profile check reports the problem against your file, against your manifest for a problem it causes in a bundled file. In a clash of shortcut, flag or role with one of your own intents, the override yields.
  • Judgment blocks intent=<name>.md apply to any intent whose manifest names a profile.

The bundled intents are written the same way; wloop profile show <owner/repo> --show renders any of them. A worked example is in Add your own intent.

intent.toml is TOML: every string is quoted, so a predicate such as when = '!pr.mine' cannot turn into something else. The loader applies exactly these defaults and refuses any other key, naming a close one when one exists. A manifest without provider takes the one defaults.toml names at the profile’s root; the bundled profile names github, and a layer’s own defaults.toml overrides it. Every bundled intent names its own provider, so a defaults.toml with provider = "slack" changes only the manifests that name none, such as yours. The profile language lists every file’s keys on one page.

KeyKindDefaultMeaning
labelstring, localizedThe name people read. Required.
summarystring, localizedOne line on what it does: its action’s tooltip, its help line, its profile listing. Required.
shortcutstringOne uppercase letter for its action, not a built-in action’s.
verbone of approve, delegate, dismissThe verb its action fulfils.
user_facingbooltrueOffered as a command, a skill and an action.
skillboolfalseInstall writes it a turn skill; needs help.target.
helptableHow its command presents it.
help.targetstringFree text saying what the command’s argument names, such as a pull request URL.
help.summarystringThe main help’s line; the intent’s summary when absent.
help.groupstringThe title of the main help’s group that lists the command, free text; action.group is the action’s.
help.exampleslist of stringsCommand lines without the command’s name.
help.docsstringA page path of the documentation site.
KeyKindDefaultMeaning
roleone of headless, attach, pr.review, pr.address, pr.nudge, probeWhat binds it to a plugin’s mechanism. One intent per role and provider.
providerone of github, session, slack, task, workspace, any, or a list of themThe units its turns serve: a provider id, a list of them, or any; defaults.toml provider when absent.
buildsone of briefA builder, never a turn.
promptfileA builder’s prompt file, through the profile layers.
schemafileA builder’s extra fields’ schema file, beside the manifest.
KeyKindDefaultMeaning
workspaceone of review, author, linked_else_main, given, or a tableWhere its turn runs, and the board it moves: a workspace builtin, or a table naming the repository.
workspace.repostringThe repository the turn runs in, owner/name.
workspace.repo_inputstringThe input that holds the repository the turn runs in.
workspace.repo_optionalboolfalseWith repo_input: a turn whose input names no repository runs in the neutral repository, with no checkout.
workspace.nametemplateThe name of the worktree created for the turn’s unit, over the name facts, with slug; the loop’s own when absent.
runnerslist of session, headlessHow its turns may run; [“session”] when absent.
sessiontableThe agent session its turns run in.
session.strategyone of reuse_live, newWhether a turn reuses the unit’s live session or starts a new one.
session.titlestringThe session’s title: 1 to 12 lowercase letters.
session.nametemplateWhat the workspace manager and the agent show the session as, over the name facts; defaults.toml session_name when absent, else the title. A name that session.title leads and the start time ends, as dd/mm hh:mm, lets the loop recognize its session when it lost track of it.
session.close_when_doneboolfalseThe session closes when its turn is done.
targettableWhat a run with no unit does.
target.missingone of capturecapture: a run with no unit captures its guidance as a new task, the turn’s unit; refused when absent.
target.titletemplateThe captured task’s title, over .guidance, with firstLine or title.
KeyKindDefaultMeaning
intakeboolfalseRuns through numbered intake.
owner_onlyboolfalseThe unit must be yours or taken over.
jointableWhen a request joins an open turn.
join.differs_onlist of stringsPersisted inputs whose change starts a new turn.
answerablebooltrueThe turn may stop on a question.
reportsbooltrueThe turn reports.
KeyKindDefaultMeaning
inputstable by nameThe turn’s inputs, by name.
inputs.&lt;name&gt;tableAn input.
inputs.&lt;name&gt;.typeone of bool, text, listThe input’s type.
inputs.&lt;name&gt;.flagstringThe CLI flag that sets it; required from the user.
inputs.&lt;name&gt;.helpstringWhat it does; required from the user.
inputs.&lt;name&gt;.fromone of user, prepareuserWho sets it.
inputs.&lt;name&gt;.persistboolfalseStored with the turn, restored on resume.
inputs.&lt;name&gt;.hiddenboolfalseLeft out of help.
inputs.&lt;name&gt;.by_hand_onlyboolfalseAn automated turn never sets it.
inputs.&lt;name&gt;.beforeone of take_overWhat runs before the turn when it is set.
inputs.&lt;name&gt;.maxinteger0A text input’s length cap in runes; 0 for none.
inputs.&lt;name&gt;.surfaceslist of cli, instructions, tryWhere it is offered; cli and instructions when absent.
KeyKindDefaultMeaning
whytemplate, localizedWhy the turn is due, over the unit’s workflow facts.
refuse[]tableA refusal of turn.start; the first whose when holds refuses with its why.
refuse[].whenpredicateWhen it refuses, over the unit’s workflow facts.
refuse[].whytemplate, localizedWhat the refusal says, over the unit’s workflow facts.
KeyKindDefaultMeaning
actiontableHow the intent’s action is offered on a row; its description is the intent’s summary.
action.groupone of start, turn, control, workspace, brief, intakestartThe group the action is listed under.
action.surfaceslist of row, detail, task, tui, menubar, notification, cli, exploreWhere the action is offered: row, detail, tui, cli and explore when absent.
action.whenpredicaterow.openWhen the action is offered on a row.
action.primarytableWhen the action is the row’s primary one.
action.primary.whenpredicatefalseWhen the action is the row’s primary one.
action.refuse[]tableA case that offers the action disabled; the first that holds says why.
action.refuse[].whenpredicateWhen the action is offered disabled.
action.refuse[].whytemplate, localizedWhy the action is disabled, over the action facts.
action.notetemplate, localizedWhat the action would do on this row, over the action facts.
action.warningtemplate, localizedShown under the action, over the action facts; empty shows nothing.
action.confirmtemplate, localizedThe dialog that confirms the action, over the action facts; empty asks nothing.
KeyKindDefaultMeaning
headlinetableThe line that opens the turn’s prompt.
headline.resumedtemplateWhat a resumed turn continues, over the turn’s facts.
headline.new[]tableA headline case; the last has no when.
headline.new[].whenpredicateWhen the headline case applies.
headline.new[].linetemplateThe headline, over the turn’s facts.
start_linestringThe intake’s first line.
resumetableWhat a resumed turn reads.
resume.changedstringThe text when the unit changed since the question.
KeyKindDefaultMeaning
guidancetableThe guidance a person gives the turn.
guidance.labelstring, localizedGuidanceThe guidance’s label.
guidance.helpstring, localizedWhat the guidance is for.
guidance.maxinteger0The guidance’s length cap in runes; 0 for none.
brieftableHow the turn reads the unit’s brief.
brief.renderbooltrueThe instructions carry the brief.
brief.waitbooltrueThe turn waits for the brief.
brief.needlist of stringsThe brief intent’s extra fields the turn needs.
KeyKindDefaultMeaning
profilestringThe intent whose judgment blocks the turn reads, or none; the configuration’s profile is the profile directory.
dimslist of stage, reviewerThe dimensions valid under its profile.
also_profile[]tableOther judgment the turn reads.
also_profile[].profilestringThe intent with judgment blocks.
also_profile[].whenpredicateWhen the turn reads the other judgment.
also_profile[].dimstableDimensions it pins, such as { stage = “first” }.
also_profile[].dims.stageone of first, follow-upThe pinned stage.
also_profile[].dims.reviewerstringThe pinned reviewer login.
also_profile[].definestringThe template define that renders it.
also_profile[].prefixstringWhat its sources are listed under; “<profile> guide: ” when absent.
KeyKindDefaultMeaning
recordtableThe turn’s record. Left out, a turn of the intent owes no record and its session’s stop is never blocked for one.
record.whenpredicatetrueWhen the turn records its outcome.
record.stateslist of done, settled, asked, failedThe states a record may take. Required when the table sets when, artifacts, outcome, require or attach_url; a table that only sets requires and missing needs none.
record.artifactslist of review-draft, triageThe artifacts a record may carry.
record.outcomelist of verdict, urlThe outcome fields a record may carry.
record.attach_urlboolfalseOn a turn whose unit is a task, a done record’s outcome url, when it names a unit such as a pull request, attaches that unit to the task; needs url in record.outcome.
record.outcome_docstringWhat the outcome holds, for the instructions.
record.history_artifactstringThe artifact the unit’s history shows.
record.require[]tableWhat a record in a state must carry; record.requires is the record kind a run owes.
record.require[].statestringThe state.
record.require[].artifactstringThe artifact it must carry.
record.require[].fieldslist of stringsThe outcome fields it must carry.
record.requiresstringThe record kind, of records.toml, a run of the intent owes; a run that ends without it opens the gate missing names. Left out, a run owes no record. Not record.require, what a record in a state carries.
record.missingstringThe gate, of gates.toml, a run opens when it fails or ends without its record, which an answer resumes; left out, such a run opens none. Required with requires.
KeyKindDefaultMeaning
gatestableThe gates its runs and drafts open.
gates.approvaltableThe approval of its gates: a rule applies to the kinds its when pins gate.kind to, else to draft gates alone when it reads a draft’s facts, else to every kind; what no rule or mode here decides, gates.toml’s [approval] does.
gates.approval.modeone of wait, delay, send, dry_run, runThe mode of every kind of gate when no rule holds; left out, gates.toml’s default of the kind.
gates.approval.windowdurationWith mode delay only: how long a person has to stop the gate’s consequence.
gates.approval.rule[]tableA rule; the first whose when holds gives the mode.
gates.approval.rule[].whenpredicateWhen the rule applies to a gate.
gates.approval.rule[].modeone of wait, delay, send, dry_run, runThe rule’s mode.
gates.approval.rule[].windowdurationWith mode delay only: how long a person has to stop the gate’s consequence.
KeyKindDefaultMeaning
effectslist of plan.cleared, turn.guidance_handed, turn.round_sent, pr.reviewed, pr.addressed, pr.nudgedWhat sending the turn applies.
draftstableThe drafts its turns’ agents propose.
drafts.allowed_effectslist of task.nudged, unit.closed, pr.nudgedThe effects its agents may declare on their drafts.
drafts.attach_postsboolfalseOn a turn whose unit is a task, the unit of each post its agent makes, sent at once or once approved, is attached to the task; reactions and edits are not posts.

An intent with builds is a builder: it needs prompt and holds no role. Any other needs workspace, [session], [headline] and [record].

[inputs.<name>] declares an input. A list input comes only from = "prepare"; a user input needs flag (--, a lower-case letter, then lower-case letters, digits or -; two manifests may declare the same flag only with the same input name and type) and help. Guidance is not an input: the text after the flags, or after --, is the turn’s guidance. --no-wait and --no-brief exist only for intents with intake = true; never declare them.

Adds another intent’s judgment to the turn: profile and when (a predicate over the turn’s facts) are required, dims pins dimensions such as { stage = "first" }, define names the template define that renders it, and prefix what its sources are listed under. The bundled address uses it for --self-review.

Makes the intent an action on rows. Its description is the intent’s summary. when offers it, primary = { when = ... } makes it the row’s first, and each [[action.refuse]] whose when holds offers it disabled with its why. note, warning and confirm are templates over the action facts.

[[gates.approval.rule]]
when = 'draft.delivery == "comment" && pr.repo_name == "docs"'
mode = "delay"
window = "10m"

The policy of the drafts the intent’s turns propose, its draft gates: the first rule whose when holds gives the mode, else mode, else gates.toml’s default for a draft gate when the table names none. A rule pinned to another gate.kind than draft fails to load: an intent’s turns open draft gates only. A rule’s when reads draft.delivery, draft.proposer and the facts of the unit the draft is on, for every provider the intent serves; never draft.purpose or draft.effects, which the turn’s agent names, nor brief.* or a fact marked Untrusted. A rule over one provider’s facts applies only to drafts on that provider’s units; a rule over two providers’ facts is refused. When the unit’s facts cannot be read, a draft whose rules read them waits for you. A rule that sends on its own (send, run or delay) applies to a draft whose delivery posts, as its plugin’s manifest marks it, only when draft.delivery == "<delivery>" is a term of its when’s top-level &&; mode alone never releases a comment or a message. dry_run stores the draft and never sends it. A workflow’s own [gates.approval], when it names a draft gate, wins over the intent’s for the turns it starts; the configuration’s workflows.<name>.gates.approval is laid over the workflow’s, or over the intent’s when the workflow names none. See the Outbox.

provider names the units the intent works on: a turn is built only on a unit of that provider, and the catalog offers the action only on its rows; any serves every provider. When several intents share a verb, the task’s key runs the one of highest rank the row enables: an intent of the unit’s own provider before one of any, and yours before a bundled one within each. So a provider = "slack" intent with verb = "delegate" replaces the bundled act on Slack tasks. An intent whose provider is not github cannot read the pr.* facts, and its templates cannot read .PR. Every template reads the unit’s wording from the provider: {{.Wording.Noun}} (“pull request”, “Slack thread”), {{.Wording.This}}, {{.Wording.Short}}, and the fact unit.ref (acme/api#7).

wloop finds the intents it runs on its own through their role, never by name: pr.review sends the sweep’s review rounds and reads their history, pr.address the address rounds, pr.nudge the nudges, probe the agent-home probe and headless a headless request that names no intent. What a role implies (its round, its reconcile, what it prepares, its context and launch, its sessions’ agent policy, whether its turns drive their workspace’s row) is the role’s, never a manifest key. Each turn stores the role its intent held, so history reads by role too. At most one intent of a layer holds a role on a provider; one of yours holding a bundled intent’s role takes it over, so an intent my-review with role = "pr.review" gets every automatic review round, and the bundled holder’s shortcut. A copy of the bundled review renamed my-review loads whole, and the bundled review keeps loading, for a turn by hand, without its shortcut or its row. When yours does not load, the bundled holder keeps them.

A workspace builtin places a pull request’s turn. On any other unit, the turn runs where the unit’s placement says, else in a neutral repository with no checkout. A workspace table names the repository instead: repo outright, or repo_input by one of the intent’s inputs. The turn runs in that repository’s main checkout when session.strategy is reuse_live, else in a worktree of its own, created by the workspace manager that keeps it. The repository must be one the manager knows. An input is set only by a person: a workflow’s turn.start refuses an intent that takes its repository from an input, so no text a workflow reads chooses where a turn runs.

Every when key is a CEL expressions, checked at load.

  • Types are bool, int, string, list (of strings), duration and timestamp. A predicate is a non-empty bool. in, size(), exists() and all() work on lists; duration("2h") and timestamp("2026-10-21T09:00:00-03:00") are literals; a timestamp minus a timestamp is a duration.
  • Fields resolve against the facts of the predicate’s slot: the action slot reads the row the daemon serves, the turn slot reads the turn being built. An unknown field fails, naming a close one when one exists. The facts page lists them.
  • inputs.<name> reads the intent’s own declared inputs, in the turn slot, with the input’s type.
  • ! applies before ==, !=, <, > and in: write !(turn.stage == "first"); !turn.stage == "first" fails to load.
  • A missing fact reads as its type’s zero value; an error at evaluation reads as false.
  • Action texts (note, warning, confirm, [[action.refuse]] why) run over a nested map of the action facts: {{.pr.slug}}, {{index .pr.nudge.who 0}}; join is the one extra function ({{join .pr.nudge.who ", "}}).
  • Headlines (line, resumed), why and [[refuse]] why run over a nested map of the facts: {{.pr.slug}}, {{.unit.ref}}, and the partial {{template "started_by" .}} (“asked for by you” or the loop’s name).
  • instructions.md.tmpl runs over the turn context with missingkey=error. .Headline, .PR, .Wording, .Guidance, .Inputs.<name> (its zero value when unset), .Resume, .History and .Command are there. The partials history, resume, record, brief, judgment, review-judgment, items and unit are wloop‘s; a profile replaces one with a file of the same name in its partials/ directory, as Change a partial shows. Include {{template "history" .}} and {{template "record" .}} in any intent that records, and {{if .Resume}}{{template "resume" .}}{{end}} in any that can be answered.
  • Every text is parsed at load and rendered against sample facts and sample turns, so a template that would fail at run time fails profile check.
  • Workspaces: review (the review workspace, created at the head), author (the workspace that develops it), linked_else_main (the workspace linked to the unit, else the repository’s main checkout), given (the workspace the request names).
  • Before: take_over.
  • Effects: the names effects and drafts.allowed_effects list are on the facts page.

shortcut is checked on every surface the action reaches (web, terminal, menu bar). The core owns every lowercase letter, every digit and its chord prefixes, and built-in actions keep their shortcuts. Bundled intents claim their shortcuts first, then yours by name. One of yours with a clashing shortcut loads without it, and profile check reports the clash.

For every row the daemon computes the actions it offers: the built-in ones, worded by the profile’s actions.toml (label, summary, note, warning, confirm, why and [<id>.input]), and each intent’s [action]. Clients render that list and decide nothing: a request for an action the row does not offer, or offers disabled, is refused with the same reason, and with a code that names the text it shows (<id>.why of a built-in action, action.refuse[<i>] of an intent’s, not_offered), which a client reads instead of the words.

Every problem is one line, file:line:col: field.path: message, sorted; profile check, doctor and the daemon’s start print them all:

intents/ask-author/intent.toml:6:10: sesion: unknown key; did you mean session?
intents/ask-author/intent.toml:15:8: action.when: 13: unknown field pr.mien; did you mean pr.mine?
intents/ask-author/intent.toml:16:8: role: "round.review" is not one of "pr.review", "pr.address", "pr.nudge", "probe" or "headless"; did you mean "pr.review"?
intents/ask-author/instructions.md.tmpl:5:6: inputs.nope is not declared

A broken intent of yours disables only itself.

Terminal window
wloop profile schema > <profile>/intents/intent.schema.json
#:schema ../intent.schema.json

Editors with Taplo then complete keys, describe each field and flag unknown keys as you type.