Skill schemas
A launchable skill can declare typed input and output contracts in its
SKILL.md frontmatter. When you run the skill with tessl launch skill --cloud, Tessl validates the launcher-supplied inputs against the declared
input schema before the skill runs, and validates the recorded result against
the declared output schema when it finishes — so a launch either conforms to the
contract or fails fast with a field-level diagnostic.
This page covers authoring the schemas block, the $ref forms it accepts, how
those refs are resolved, and the structured result a launch returns.
Authoring the schemas block
Declare schemas under a single top-level schemas key in the skill's SKILL.md
frontmatter. It has two optional slots, inputs and output:
Each slot is either an inline JSON Schema or a single $ref reference.
Both slots are optional and independent — declare one, both, or neither:
schemas.inputs— validated against the launcher-supplied inputs (theINPUTSobject, keyed by the skill's own placeholder names) before the skill runs. A missing required input fails the launch during preparation rather than being discovered mid-run.schemas.output— describes the structured payload the skill produces. It is composed into the returned result (see The returned result).
A slot may be written inline instead of via a $ref:
For a complete, copyable example — a greet skill with both slots and a bundled
./schemas/ directory — see the
quickstart-typed-skill tile.
$ref forms
A slot's $ref must be a non-empty string and the sole key of the slot
(you cannot mix $ref with other keys). Three forms are accepted:
Anything else (a bare name, an http:// URL, a fragment-only #/... pointer)
is rejected.
Local $refs are contained to the skill bundle
A local $ref is resolved relative to the directory holding the SKILL.md, but
it is contained to the skill's bundle: a ref may point at a sibling or a
shared file elsewhere in the same bundle (../shared/output.schema.json), but
one that escapes the bundle directory — lexically or through a symlink — is
rejected. This lets skills that genuinely share a contract stay DRY by pointing
a local $ref at one shared file, without a ref reaching outside the bundle.
Remote $refs are fetched safely
A remote $ref is fetched only at launch preparation time (never during
plain validation), over a guarded HTTPS client:
- HTTPS only, with no embedded credentials.
- No private or internal targets —
localhost, private/reserved IP ranges, and link-local / unique-local IPv6 are refused. The host is re-resolved and pinned to the validated address at connection time, so a DNS-rebinding trick cannot swap in an internal address after the check. - Bounded — a 10-second timeout and a 1 MiB body cap; HTTP redirects are refused outright.
The fetched body must be valid JSON describing a JSON Schema object.
Resolution precedence
When a launch prepares a skill, each declared slot is resolved into a fully inline JSON Schema:
- Read the
schemasblock from the skill'sSKILL.mdfrontmatter. A skill with noschemasblock declares no contract — its inputs are not validated and its output is left unconstrained. - Resolve each slot by its form: an inline schema is used as-is; a local
$refis read from the bundle; a remote$refis fetched. - Inline the resolved schema. Resolution is single-level — a
$refinside a resolved schema document is not followed. Author self-contained schema files.
If a slot cannot be resolved (a malformed block, an unreadable local file, an unfetchable or unsafe remote URL), the launch fails during preparation with a diagnostic naming the failing slot, rather than proceeding with a partial contract.
The returned result
Every cloud launch returns an envelope, regardless of whether the skill
declared any schemas. It is one of four shapes, chosen by its status, and each
shape carries only the fields listed for it:
statussays what the skill did.succeeded: it did its work.no_action: it ran but made no change.needs_input: it asked a blocking question and stopped.failed: it tried and could not complete. This is the skill's own account, distinct from the launch run's own status.summaryis a short human-readable sentence, required on every shape.questionscarries the exact questions the skill asked, onneeds_input.reasonsays why the skill could not complete, onfailed.outputis the skill-governed structured payload, onsucceededonly.
output appears when the skill declares a schemas.output: that resolved
schema becomes the output property and output becomes required, so the
skill must produce a payload conforming to its own declared schema and the
launch validates it. When the skill declares no output schema, output is not
permitted at all. output belongs entirely to the skill, so there is nothing
for it to hold when the skill has not said what it holds. A skill that needs to
report structured detail on a non-succeeded outcome puts it in summary or
reason.
needs_input is only available to a launch that can route the answer. A
task turn can put a blocking question to a human and resume, so its contract
carries that shape; an interactive or scheduled launch cannot, and its contract
omits it. On those, record the blocking question as failed with the question
in reason.
Consuming the result from the CLI
tessl launch skill --cloud renders the returned result when a launch
completes, and --json emits the whole envelope for a script to consume:
See tessl launch skill for the full flag
reference.