Skip to main content
This page lists every key the config checker accepts. It’s the same checker on the desk, in jun push and in jun eval: an unknown key, a wrong type or a value out of range is an error that names the file, and the config isn’t saved until it’s fixed. The agent-config format is MIT-licensed.

Folder layout

Any other path is an error: Unknown file. Allowed: instructions.md, desk.yaml, website-actions.yaml, tasks/<name>.md, actions/<name>.yaml, tests/<name>.yaml, widgets/<name>.widget (lowercase names).
Configs from before October 2026 used skills/<name>/SKILL.md, tools/ and evals/, and kept the instructions in AGENTS.md. The desk still accepts those paths from jun push and stores them in the layout above; jun pull moves your local files. An AGENTS.md counts as the instructions only when there’s no instructions.md.
jun pull and jun init also write three generated files that aren’t config: AGENTS.md (a guide to this format for coding AIs), CLAUDE.md (@AGENTS.md, for Claude Code) and README.md. jun push never sends them and the desk doesn’t store them. See Edit with your own AI.

instructions.md

The AI’s instructions. Optional YAML frontmatter sets guardrails; the Markdown body is how the AI talks to customers and what it must and mustn’t do. This is the starter:
instructions.md
integer
default:"8"
How many times the AI answers in one conversation before it hands off to a person. A whole number from 1 to 50. Extra turns the AI takes to continue a run of website actions don’t count.
string[]
Topics the AI always hands to a person, in plain words. Up to 30 are used.
No other frontmatter key is allowed. The frontmatter itself is optional. In the body, @lookup_order refers to the action of that name. See Instructions.

tasks/<name>.md

A task: a procedure in Markdown. The file name is the task’s name. The frontmatter says when it applies; the body is the steps.
tasks/refund.md
string
required
When this procedure applies. At most 1,024 characters. The AI reads it to choose the task.
string
Optional. If present, it must equal the file name.
The body must not be empty. Other fields (license, metadata, …) are allowed and ignored.

Intent keys

A task can define an intent: your app opens a chat for that purpose with JunDesk.open({ intent, onExit }), and the chat starts with the task’s opening line and quick replies. See Intents.
tasks/cancellation.md
string
The intent name your app passes to JunDesk.open. Lowercase letters, digits, _ and -, at most 40 characters. One task per intent.
string
The chat’s first line, shown without calling the AI. One line, at most 300 characters.
string[]
Quick replies under the opening. At most 8, each one line of at most 60 characters. Needs opening.
string
A button label that stays on screen for the whole chat. One click closes the chat and calls your onExit. At most 40 characters.
opening, replies and exit are errors without intent.

actions/<name>.yaml

An action: one HTTP request the AI may make. The file name is the action’s name. This is the starter, actions/lookup_order.yaml.example:
actions/lookup_order.yaml

Keys

string
required
What the action returns and when to use it. The AI reads this to decide when to call it.
GET | POST
default:"GET"
The HTTP method.
string
The address to call, starting with https:// (or http://), with {placeholders} for inputs. Required, except for an action that only shows its widget (see widget). Inputs in the URL are URL-encoded.
map
Request headers. The place for keys: {secrets.NAME} and {user.secrets.NAME}. accept: application/json is sent by default.
map
Query parameters, with placeholders. A parameter whose value comes out empty is left out.
any
The JSON body, for POST only (an error with GET). Placeholders work at any depth; a value that is exactly "{name}" keeps the input’s type, so numbers stay numbers. Without body, a POST sends the inputs as a JSON object.
map
The values the AI fills in, by name (letters, digits and _). Each is a map with the keys below, or just a type (orderNumber: string).
string | number | integer | boolean
default:"string"
The input’s type.
string
What the value is, for the AI.
boolean
default:"true"
false lets the AI leave it out.
string[]
The only values allowed.
string[]
Keep only these top-level fields of a JSON response for the AI. Responses are cut at 4,000 characters either way.
any
A canned response, used instead of the real call by jun eval --mock-tools and by the test chat’s safe mode.
string
default:"Looking that up"
The one line the customer sees while the action runs. Plain text, one line, at most 60 characters, no {placeholders}. The customer never sees the action’s name, inputs or result.
string[]
Offer the action only on these page paths of your site. Each starts with /; a trailing * matches everything under it (/orders/*). The page comes from the widget’s latest snapshot, so the action isn’t offered when the page isn’t known.
string
Show a successful result to the customer as widgets/<name>.widget. The file must exist. With widget and no url, the action calls nothing: the card is filled from the widget’s example data and the AI’s inputs.
string
When a widget’s button ran this action and it worked, the line the card folds to. A Jinja template over the values entered and the result, at most 200 characters, like Thanks {{ name }}, we'll be in touch. The AI doesn’t answer the press.
string
When a widget’s button ran this action and it failed, the line shown on the card, which stays open. Same rules as widget_done.

Placeholders

Every {placeholder} in url, headers, query and body must be one of these, or the file is refused: Key names are UPPER_SNAKE_CASE. When the customer isn’t signed in, an action that uses {user.…} returns an error to the AI instead of calling your API. See Identify users and Secrets and settings.

Reserved names

An action can’t be called handoff, flag_problem, jun_done, show_widget or activate_skill: those are the AI’s own tools.
Actions have no confirm or risk setting. The AI calls them when it decides to. Write the confirmation into the task (“say exactly what changes and wait for a clear yes”), as the starter apply_save_offer does, or put the step behind a widget’s button. Website actions are different: see website-actions.yaml.

widgets/<name>.widget

A card in OpenAI’s ChatKit widget format: the file you get from Download in ChatKit Studio, or one you write by hand. It’s JSON whose template is a Jinja template that renders to the card’s component tree.
widgets/order.widget
"1.0"
required
The only supported version.
string
required
A Jinja template that renders the card’s JSON. The data is the action’s JSON response: an object as is, a list as items, anything else as value. Write values with | tojson.
string
The widget’s title on the desk. Defaults to the file name.
string
One line of plain text for the chat’s folded island, from the same data: a Jinja template of at most 200 characters. The line is cut at 80 characters. If it fails to render, the card still shows without it.
object
Example data, for previews and checks. When present, the template (and summary) must render with it, or the file is refused.
object
The data’s JSON Schema (ChatKit Studio’s key). When present, sample and every state must fit it. Without it, the desk works out the parameters from sample.
{ name, data }[]
More named examples, at most 10. Each must fit the schema and render. ChatKit Studio’s own states (inside encodedWidget) are used when the file has no states list.
boolean
true lets the AI show this card with data it fills in itself, through its built-in show_widget tool. Needs sample, since the AI fills the card in its shape.
string[]
Website actions whose result this card shows, at most 10 names. One card per website action across all widgets.
ChatKit Studio’s download also carries encodedWidget and outputJsonPreview. They’re allowed: without sample, the desk uses Studio’s default state as the example data, and Studio’s rendered preview for the card’s preview.

What a rendered card must be

  • The root is a Card, ListView or Basic.
  • Only these components: Basic, Card, ListView, ListViewItem, Box, Row, Col, Form, Spacer, Divider, Transition, Table, Table.Row, Table.Cell, Text, Title, Caption, Markdown, Label, Badge, Icon, Image, Chart, Button, Input, Textarea, Select, DatePicker, Checkbox, RadioGroup.
  • At most 400 components, 24 levels deep, 48 KB of JSON.

Button actions

A button’s onClickAction (or a form’s, list item’s or card’s confirm) has a type: An action with "handler": "client" goes to your page instead (JunDesk.onWidgetAction). See Widgets for the full behaviour.

The Jinja subset

Templates run in Jun’s own Jinja interpreter. It’s lenient where real API data is: a missing field is undefined, and undefined | tojson is null. It supports {{ }}, if/elif/else, for with loop, set, slices, ~, and/or/not, is tests and inline if … else. Filters: abs, attr, batch, capitalize, center, count, d, default, dictsort, e, escape, first, float, indent, int, items, join, last, length, list, lower, map, max, min, reject, rejectattr, replace, reverse, round, safe, select, selectattr, sort, string, sum, title, tojson, trim, truncate, unique, upper, wordcount. Tests: boolean, defined, divisibleby, eq, equalto, even, false, float, ge, gt, in, integer, iterable, le, lower, lt, mapping, ne, none, number, odd, sameas, sequence, string, true, undefined, upper.

website-actions.yaml

How each action on your website runs, over what your site registered. Set it on the desk under Agent → Actions → On your website, or by hand. A map of action name to auto, confirm or human, or to a map with run and status:
website-actions.yaml
string | map
The website action’s name as your page registers it (letters, digits, _, ., -, up to 64 characters).
auto | confirm | human
auto runs without asking. confirm shows a summary and a Confirm button first. human sends the request to your team.
string
The line the customer sees while it runs. One line, at most 60 characters.
Actions not listed keep the risk your page gave them (confirm by default). See Page actions.

tests/<name>.yaml

Tests: a YAML list of cases. Each has the customer’s message (or several) and what a good reply does.
tests/basics.yaml
tests/cancellation.yaml

Case keys

string
Shown in results. Defaults to case <n>.
string
The customer’s message. Use this or messages.
string[]
Several customer messages in a row; the AI answers each in turn and expect is checked on the last reply. A handoff ends the case early.
string
Run the case as a chat your app opened with this intent.
string
The page path the customer is on, like /pricing, for actions and website actions with pages.
list
Website actions on the customer’s page for every turn, in the WebMCP shape. Each needs name and description, and may have inputSchema, risk (auto, confirm, human), context, key, annotations and mock (a string: what the page returns when it runs, fed to the next turn as if the customer confirmed).
map
What a good reply does. Every key given must hold.

expect keys

answer | handoff | escalate | action
answer: the AI replies. handoff: it hands off to a person. escalate: it flags a problem to the team (flag_problem). action: it proposes a website action.
string[]
Actions the AI must call (it may call others too).
string
A sentence describing a good reply. A judge model checks the reply against it.
string | { name, input }
The website action the reply must propose, and optionally input values it must have filled (compared exactly).
string
The widget the reply must show.
A website-action case:
tests/cart.yaml
A message that asks for a person (“can I talk to a real person”) hands off straight away, as on the live desk, without calling the model.

Desk settings

desk.yaml holds the desk’s settings that are plain choices: the AI’s switches, the chat widget, business hours and assignment. With it, a whole desk can be set up from git, or copied to another desk with jun pull and jun push.
desk.yaml
How it works:
  • A key you leave out is left as it is. So is a whole section, and so is everything when the folder has no desk.yaml. A partial file only changes what it names.
  • Saving it changes the desk. When a version is saved (jun push, the Agent page, or Code on the Chat widget page), the keys whose value changed since the previous version go live. Visitors see widget changes within a minute.
  • The settings pages write it too. Saving on the Chat widget page, Settings → AI, Business hours or Assignment saves a new version with the changed keys edited in place: your comments and key order stay. A key the file doesn’t have is added when its setting changes.
  • Every desk has one. Until it’s saved, the desk writes it from the current settings when you open the Agent page’s Code view or run jun pull.
Keys are checked like every other file: an unknown key or a value out of range is an error that names the key, like chat.mode: Mode must be island or window.

ai

boolean
The AI answers new chats. false: every new chat goes to your team.
string
The model every AI job uses, from your provider. default (or empty) uses the provider’s default. Letters, digits and . : / @ - _, up to 100 characters.
map
A model per job, over ai.model: answer, brief, nudge, draft, topics, judge, suggestions, followups, editor. The map is replaced as a whole; {} clears every override. See Models.
integer
AI replies a month. From 0 (no limit) to 1,000,000.

chat

The chat widget, as on the Appearance page and the Install tab.
string
The name in the chat header. Up to 80 characters; "" uses your workspace’s name.
string
The brand colour, a six-digit hex colour like "#5b4cff" (quote it: # starts a comment in YAML).
island | window
island: a pill at the bottom centre. window: a button in the corner that opens a chat window. See Modes.The old launcher key is still read: island and bar mean mode: island, card means mode: window with greetingCard: true, and button means mode: window with greetingCard: false. The desk never writes it: the next time it writes the mode, launcher: is replaced by mode and greetingCard.
boolean
Window only. true: a greeting card over the button. false: just the round button.
logo | text
Window only, with the greeting card off. What the round button shows: your logo (a chat bubble until you upload one) or “Chat with us”.
dark | page
Island only. The island’s colour: near-black on every page, or following the theme.
boolean
Island only. The island’s turning multi-colour border.
auto | light | dark
auto follows each visitor’s system setting.
integer
Corner rounding, from 0 to 24.
left | right
Window only. The side of the page.
boolean
A soft chime for new replies.
string
Up to 200 characters. Empty uses the default words.
string
Under the name in the header. Up to 80 characters.
string
The message box’s placeholder. Up to 60 characters.
string[]
Up to 4, each up to 80 characters.
boolean
Ask “How did we do?” when a conversation is resolved.
string[]
The websites that may show the chat, like acme.com or "*.acme.com". [] means any website. See Install.
boolean
Offer help when something breaks on the page. Page openers show only while this is on.
boolean
Let the AI read and use your website: page control. On by default. A site’s snippet can still leave itself out with data-page-control="off".
"allow-always" | "every-time" | "never"
Before page control clicks, types or chooses: ask with an Always allow button (the default), ask every time, or don’t ask. See Page control.
list
Page openers, at most 10. Each has page (a path pattern: /pricing, /docs/*, *), after (seconds on the page, 5 to 600), and either message (fixed text, up to 140 characters) or no message, so the AI writes the line, with an optional hint of what to offer (up to 200 characters). The list is replaced as a whole.

hours

boolean
Use business hours. false: the desk counts as always open.
string
A time zone name, like Europe/London, America/New_York or UTC.
map
Open days, monday to sunday, each "HH:MM-HH:MM" in 24-hour time ("24:00" for midnight). Days not listed are closed. The map is replaced as a whole.
string
Sent once per closed period to visitors writing to your team. {when} becomes the next opening. Up to 500 characters.

inbox

manual | round_robin
Who gets a chat that needs a person. See Assignment.
integer
With round robin, the most open chats a teammate is given. From 0 (no limit) to 100.

Not in the file

  • Logos. Images are uploaded on the Chat widget page.
  • Keys and secrets: the identity secret, issue-tracker tokens, action keys, the OpenAI key and the ChatGPT sign-in. They never go in a file that may end up in git.
  • The AI provider. It depends on those credentials; choose it in Settings → AI.
  • People: team members, invites and passkeys, and each person’s notification settings.
  • Knowledge sources and the install snippet.

README.md, AGENTS.md and CLAUDE.md

Generated by jun pull and jun init, and rewritten on every pull: a guide to the files for people (README.md) and coding AIs (AGENTS.md, and CLAUDE.md, which imports it). They aren’t config: jun push skips them, and the desk drops them if an older CLI sends them. Versions saved with a README.md before October 2026 read without it.

jun CLI

Pull, push and test these files.

Actions

Building actions on the desk, keys and Try it.