Subcore AI Docs
Agents API

Prompt files

What goes inside a stored file. Transcluding another file, binding a variable, and the mistakes the deployment refuses.

A prompt file is markdown. Your deployment compiles the files for an agent into one prompt per channel when a call arrives, and markdown is almost all of what it does: the text you write is the text the model gets.

Two things are not markdown, and they are the reason this page exists. A line holding [[path]] pulls in another file. A {{name}} token stands in for a value the deployment works out at runtime, and it has to be declared in the file that uses it.

---
agentNameIntro: agent.nameIntro
---
# Who you are

{{agentNameIntro}}You are the front desk of a physiotherapy clinic. You book
and change appointments and take messages. You are not a clinician.

[[hours]]

[[kit/identification-guards]]

Nothing on this API compiles what it stores, so every rule on this page is checked at your deployment rather than at write time. A file that breaks one is stored with a 201 and fails that agent's calls. The test turn is where you find out, and it names the file and the line.

Pulling in another file

A transclusion is a line whose only content is [[path]]. Spaces or tabs around it are fine. The whole line is replaced by the compiled contents of that file, so the blank lines you leave around it are the only thing joining one file's text to the next.

Everything above stands on its own.

[[hours]]

Everything below does too.
RuleDetail
.md is optional[[hours]] and [[hours.md]] name the same file
Paths are lowercaseThe grammar is [a-z0-9][a-z0-9._/-]*. [[Hours]] is not a transclusion, and fails as described below
Slashes are allowed[[kit/identification-guards]] reaches a file in a subdirectory
It has to be the whole lineThere is no inline form. See below
An empty file is allowedIt contributes nothing, leaving a blank line where the line was

A file may be pulled in from more than one place. It is inlined every time rather than once, so two parents that both want a shared section each get it. A file that eventually pulls in itself is refused as a cycle, and nesting is capped: an entry file may reach nine levels below itself, and the tenth is refused.

What a transclusion can point at is the agent's whole file set, which is the deployment's own files with your stored ones laid over them by path. That includes a file only you store, so a stored file can pull in another stored file, and it includes the deployment's shared pieces, so yours can pull in one of those without copying it.

Writing the brackets literally

Because there is no inline form, a [[ anywhere other than a transclusion line is a mistake rather than text, and the deployment refuses the file instead of sending brackets to the model. To write the characters, escape the opening pair:

Write \[[hours]] on a line of its own to pull the hours in.

That reaches the model as [[hours]]. The escape is only needed for [[; a single [ is ordinary text, and markdown links are untouched.

Variables

A {{name}} token is a placeholder. It survives compiling as it is written and is filled in afterwards, from a resolver the deployment provides. The file itself stays plain data, which is why a stored file can use a value without being able to run anything.

Every token is declared in frontmatter: a --- line at the very top of the file, the declarations, then a closing ---. The block is stripped out, so it never reaches the model.

---
title: Front desk identity
agentNameIntro: agent.nameIntro
today:
  from: date.today
  args: { timezone: America/New_York, format: long }
---
{{agentNameIntro}}Today is {{today}}.

Each declaration binds the local name on the left to a resolver key on the right. The short form is one line. The long form indents from, and takes arguments as well.

A key beginning tenant. is the one kind you set yourself. It reads that agent's own prompt variable of the same name, so one stored file renders differently for each agent your deployment serves:

---
clinic: tenant.clinicName
---
You are the front desk of {{clinic}}.

A tenant. binding needs someone to answer it. Normally that is the agent's own variable of the same name, and if the agent does not carry one the file still compiles and every call that agent takes then fails, with Unknown resolver "tenant.clinicName" naming the key. The exception is a deployment that registers a tenant. resolver of its own as a default, which then answers instead; an agent's own variable still wins whenever it has one.

A deployment older than kit 0.21.1 never turns an agent's variables into resolvers at all, so the binding gets nothing from the agent however many it carries. Upgrade the deployment first, then store the variable, then store the file that binds it.

FieldWhere it goesMeaning
fromInside a declarationThe resolver key, dotted identifiers such as date.today. Required in the long form
argsInside a declarationA single line of { key: value } pairs. Values are text, numbers or true and false, and nothing nested
atInside a declarationLeave it out. See the callout below
title, descriptionBeside the declarationsNotes for whoever reads the file next. Ignored when compiling

The first three are the fields of one declaration, and they are the only three a declaration takes. title and description are not fields of a declaration at all: they sit at the top level alongside the names you are binding, and putting one inside a declaration is refused like any other unknown field.

The names themselves are yours to choose, so the top level takes any of them, as long as each is a plain identifier that either names a resolver key on the same line or opens a declaration with from: under it. A line of another shape fails the compile: a name with punctuation in it, a value that is not a resolver key, a line with no colon, an indented line where a name should be, a list, or anything nested.

That is most of what makes a YAML document pasted at the top of a file fail rather than leak into the prompt, but not all of it. A run of plain key: value pairs, where every value happens to look like an identifier, is read as declarations and compiles. It fails on the first call instead, as resolvers nobody registered.

at: session is reserved and does not work yet. A file that sets it compiles and then fails every call the agent takes, with a message about session-time resolution. Leave at out, which gets you the behaviour you want: the value is worked out when the deployment starts.

The rules that catch people

The binding travels with the text. A token has to be declared in the same file that writes it, not in the file that pulls that file in. It is the reason a file can be moved between agents without carrying a hidden dependency.

One name, one meaning. Two files compiled into the same prompt may both declare company, and it is fine as long as they declare it identically. If they point it at different resolvers, the compile is refused, because by the time the value is filled in there are no file boundaries left to tell the two apart.

A name outside the grammar is not a token at all. Names match [a-zA-Z_][a-zA-Z0-9_]*, so {{clinic_name}} is a token and {{clinic-name}} is not. The second is never checked, never filled in, and reaches the model as the literal characters you typed. Nothing refuses it, so it is worth a second look at any name with a hyphen or a leading digit in it.

To write {{ literally, escape it as \{{. An escaped token is not a usage and needs no declaration.

Which resolver keys exist

Two kinds, and only one of them is yours to set.

tenant.<name> reaches a variable you store on the agent itself, so this is the namespace to reach for when a fact differs between two agents. Store clinicName in the agent's variables and a file binds it as tenant.clinicName. One file can then be shared by every agent while each answers with its own values.

Everything else belongs to the deployment. The kit ships four, so these work anywhere unless your deployment has replaced them:

KeyGives you
agent.nameThe deployment's configured display name
agent.nameIntroA leading sentence naming the agent, or nothing when no name is set
date.todayToday's date. args: { timezone, format }, where format is long, short or iso
date.nowThe current time. args: { timezone }

Anything beyond those four is a key the deployment registered in its own code, and the deployments doing real work tend to have several. Ask whoever runs it for the list, or read an existing file's frontmatter to see which keys are in use. The two namespaces are kept apart on purpose: a variable you store lands under tenant. and nowhere else, so it can never take over a key like agent.name that the deployment's own code depends on. Within tenant. it is the other way round, and deliberately so: an agent's own value overrides a default the deployment registered under the same name.

Storing a file never adds a key. A binding that nothing answers, whether a deployment key that does not exist or a tenant. name neither the agent nor the deployment supplies, compiles cleanly and then fails every call that agent takes, with a message naming the key. So a file and the variables it binds are one change: store them together, and test the agent before a call arrives.

What compiling changes

Very little, and the parts it does change are worth knowing when you are comparing bytes:

  • The frontmatter block is removed.
  • Windows and classic Mac line endings become newlines.
  • Blank lines at the very start and end of each file are trimmed. Blank lines inside are left exactly as written.
  • Each transclusion line becomes the file it named.
  • Nothing else is touched. The compiler is not a markdown parser, so a transclusion alone on a line still counts inside a code fence, and headings, lists and emphasis are passed through as text.

When something is wrong

All of these are refused at the deployment, on the first call after the file is stored. On the test turn they come back as a 500 whose code is compile, with a message naming the file and usually the line.

What you wroteWhat you get
A transclusion naming a file no one suppliesMISSING_FILE, naming the path and the file that asked for it
Files that pull each other in, directly or around a loopCYCLE, with the chain
More than nine levels of nesting below the entry fileDEPTH_EXCEEDED
[[ on a line that is not a transclusion, unescapedINLINE_IMPORT, with the line number
A {{token}} with no declaration in that fileUNDECLARED_VARIABLE, with the line number
One name declared two ways in one promptBINDING_CONFLICT, naming both files
Frontmatter that is unclosed, a name outside the grammar, a declaration field other than from, args or at, or args that are not one line of scalarsMALFORMED_FRONTMATTER, with the line number
A binding naming a resolver the deployment does not haveA failed call naming the key. This one is not a compile error, so it has no code of its own
A tenant. binding neither the agent nor the deployment answers. Before kit 0.21.1 the agent side never counts, so only a deployment default can answer oneThe same failed call, Unknown resolver "tenant.<name>". Store the variable first, or upgrade the deployment

Two more are warnings rather than refusals, and your deployment logs them: a file pulled in twice, and a name declared but never used.

Storing a file never produces any of these. The write routes check the path and the size and store the bytes, so a 201 means the file is saved, not that it works. See what the write routes do check.

On this page