Authoring a Flight Plan
This guide walks you from an empty file to a skill ready to freeze. By the end you will have a single SKILL.md document that declares the actions it calls, the environment it runs in, the inputs it resolves, the outputs it produces, and a deterministic step graph that wires them together with no language model in the loop.
If you have not read it yet, start with Flight Plans for the model. This guide is the how; that page is the what and why. The normative field reference is the Flight Plan Manifest Spec, and the companion guides downstream are Freezing a Flight Plan and Launching a Flight Plan.
This guide covers authoring by hand. The same manifest is also the form an AI-assisted authoring flow prompts an operator to fill in, but the artifact is identical either way.
What you are building
A Flight Plan is a skill before it is sealed. The skill is one SKILL.md document in the agentskills.io ↗ format, extended with a single Aileron-specific block. You author the skill. A freeze step turns it into a Flight Plan by resolving images to digests, locking, binding the execution environment, attaching the trust contract, and signing.
This shapes how you author. You write the contract; you do not write the seal. The requires: block, the inputs, the outputs, the step graph, and the per-action trust contract are yours to write. The lockfile, the digest pins, and the signature are produced by freeze, never hand-written.
The Aileron extension lives under one namespaced aileron key in the frontmatter. The extension is lossless if stripped. A tool that ignores the Aileron fields still reads a valid skill, and a host that lacks a named action reports a missing requirement rather than refusing to load the document.
The skeleton
A complete, valid minimum:
---
name: weekly-metrics-digest
description: Read a metrics window, summarize it, and file a tracking issue.
aileron:
schemaVersion: aileron.flightplan.v1
requires:
actions:
- ref: aileron:metrics.read_window
trustContract:
credential:
kind: oauth2
placement: header
oauth:
scopes: ["metrics.read"]
hosts: ["metrics.example.com:443"]
effect: read
idempotency:
safeToRetry: true
audit:
fields: ["network-target", "operation-effect", "response-summary"]
environment:
tools:
- [email protected]
inputs:
- name: window_days
type: number
description: How many days of metrics to read.
resolution:
rule: literal
default: 7
outputs:
- name: digest
mimeType: text/markdown
encoding: utf-8
publish:
target: file
path: digest.md
steps:
- id: read
kind: action-call
actionRef: aileron:metrics.read_window
args:
days: inputs.window_days
- id: summarize
kind: transform
bindings:
rows: steps.read.messages
outputs: ["text"]
materializesOutput: digest
---
# Weekly Metrics Digest
This skill reads a recent metrics window, writes a short digest, and files a
tracking issue that links to it.Open the frontmatter block at the top; close it; write the body underneath. The aileron block is the only Aileron-specific part. The keys it sits beside (name, description, and the rest) are the agentskills.io skill keys.
The requires: block
The requires: block lists the actions the plan calls. The sibling environment: block declares the container it runs in. Together they are the dependency declaration freeze reads when it pins and binds.
The requires: block is optional. A tool-only plan whose every effectful step is an in-container tool step dispatches no connector action, so it declares no actions and omits the block entirely. Do not carry a dummy action ref to satisfy the schema: an unused ref names nothing the plan calls and surfaces a spurious “not satisfiable here” warning at aileron skill install. Declare an action only when a step actually calls it.
Actions and the per-action trust contract
Each requires.actions[] entry names one action by ref and carries a per-action trustContract. The ref is aileron:<connector>.<action>, and it attaches to the action model. An unsatisfied requires: entry is a missing-requirement signal the runtime surfaces, never a parse failure. The declared set stays load-bearing for a plan that does call actions: the runtime refuses an action-call step whose ref is not declared here, so every called action must appear.
The trust contract is the security surface for that action. You write it; freeze attaches it; the detached signature is the human attestation that it is correct. Every field below records something the runtime enforces or the user sees.
credentialdeclares the credentialkind(none,api-key,oauth2,aws-sigv4) and itsplacement(header,query,cookie,body,signing,session). It never carries a credential value. Anoauth2credential is always placed in aheader; anaws-sigv4credential is always placed viasigning.oauthis required whencredential.kindisoauth2. It declares the scopes, endpoints, and refresh behavior, never a token.hostsis the closed list of upstream hosts the action reaches, ashost:portpairs. The declared host set IS the security boundary. The runtime grants nothing undeclared.pathsoptionally narrows the request paths on those hosts.effectis one ofread,write,delete,spend,external-send. It drives default approval routing. Areadruns unattended; the other four raise an approval gate at launch.idempotency.safeToRetrydeclares whether re-running with the same inputs produces no additional effect.idempotency.idempotencyKeydeclares whether the operation accepts a client-supplied key. Read ops are typically safe to retry; writes that mutate the world are not.redactionoptionally lists rules applied to response fields before they reach the agent or the author. Adroprule removes a field, amaskrule replaces its value, and ahashrule replaces it with a stable hash.verificationoptionally declares a read-only probe the runtime can call to confirm the credential still works.audit.fieldsdeclares which of the closed audit-record fields this operation emits. The closed set isconnector-hash,action-manifest-version,credential-binding,identity-label,approved-input,approval-decision,network-target,operation-effect,request-summary,response-summary, andresult.
Declare the minimum each action needs. The contract is verbose by design. A plan with many actions records many such blocks, and that is the cost of a per-action audit story the user can read.
The execution environment
The environment: block declares the single container the plan runs in. It is a sibling of requires: under the aileron block, not nested inside it. The block is optional; omit it for a plan that needs no tooling. When present, it declares tools, an image, or both, and must declare at least one, so an empty environment: {} is rejected.
environment.toolsnames curated-catalog tools the plan’s steps invoke, each as<name>@<version>(for example[email protected]or[email protected]). The name set is closed to the curated catalog; an unknown name is rejected at validation, before freeze. The version grammar is loose (2,2.x, and2.19.1all validate). Freeze resolves each entry to its catalog devcontainer Feature and composes one image on the Aileron-provided runner base.environment.imagenames a custom base image, the escape hatch when the curated catalog does not carry the tooling you need. Freeze resolves a tag to animage@sha256:digest pin. Declaring bothtoolsandimagecomposes the declared tools onto your custom base.
Freeze resolves the declared environment to a single content-addressed digest, and launch boots that one image and runs the whole plan inside it. The execution container is agent-free. The image carries no coding agent. The Flight Plan runs composed steps, not an interactive agent session.
The environment block never names an identity. The credential binding lives on your machine at launch, not in the plan, so the identical sealed artifact runs under different vault-bound identities and the audit trail records who ran it.
Inputs
Every input the plan depends on is declared in inputs[], each with a resolution rule. A value that varies by use case, such as a time window, is a declared input rather than a constant baked into the composition, so one composition serves many operators.
nameis the input name, unique within the manifest.typeis one ofstring,number,boolean,timestamp,object,array.descriptionis optional human-readable semantics.resolutionis the resolution rule, discriminated by itsrulefield.
There are three resolution rules.
literaltakes a value passed at launch. An optionaldefaultapplies when no value is passed.dynamicresolves a launch-relative value once at launch. Itsvalueisnow(the launch timestamp) ortoday(the launch date).sourcereads from a live source. Itssource.actionRefnames the action whose result resolves the input, with an optionalsource.select.
Inputs resolve once, at the launch boundary, into a concrete resolved-input set. Two steps that read the same dynamic input see one value. A source read happens in the resolution phase, before the step graph walks, so it is not a graph edge.
Outputs
The outputs[] block is the declared contract for what the plan produces. It is kept distinct from the transport so the transport can change later without changing the contract.
nameis the artifact name, unique within the manifest. The audit references artifacts by this name.mimeTypeis the artifact media type.encodingisutf-8orbase64. v1 implementsutf-8only.base64is reserved.publish.targetisfileornone. Whentargetisfile,publish.pathnames the output file.
A step result reaches a declared output through the step’s materializesOutput field, which names a declared outputs[].name.
The deterministic step graph
The steps[] block is the composition. It wires declared actions and deterministic transforms into a directed acyclic graph. The block is optional. A skill with no steps block is instruction-only and still a valid manifest.
Every step carries an id unique within the graph and a kind from a closed enum. The kind is what makes the no-LLM guarantee structurally checkable.
kind | Reaches an LLM | Semantics |
|---|---|---|
action-call | No | Invokes a declared action. Its actionRef names the action and its args bind the action’s arguments. |
transform | No | Runs deterministic no-LLM logic over data already in the graph. It has no host, network, or credential surface. |
tool | No | Runs a declared environment tool as a deterministic subprocess inside the booted plan container. Its command is an argv array run with no shell interpretation. Its optional mount and collect are the file-I/O boundary, and its optional trustContract declares the step’s network reach. |
llm-seam | Yes | The single marked non-deterministic seam. The only kind that reaches an LLM. |
A tool step runs one of the tools your environment declared. Give it a command argv array (the program then its arguments, never a shell string), and optionally a mount to place input files into the container and a collect to read a produced path back as the step output. If the tool reaches the network, declare its reach with a per-step trustContract whose hosts list the upstream it may call. Freeze seals that reach into the lock, and launch enforces it: a scoped call to an undeclared host is refused at the daemon proxy before any TLS handshake, and no credential bytes ever enter the container.
Wire steps with bindings, never values. A binding is a reference. An action-call step binds its args; a transform, a tool, and an llm-seam step bind their bindings. A binding takes one of two forms.
inputs.<name>references a declared input resolved once at the launch boundary.steps.<stepId>.<outputName>references a named output of a prior step.
The references make the wiring a graph. The runtime executes it in deterministic topological order, breaking ties by declaration order.
Three rules a JSON Schema cannot express, that the freeze lint checks and the runtime enforces:
- The graph is acyclic.
- An
action-callstep’sactionRefmust match a declaredrequires.actions[].ref. - An
llm-seamis the only kind that may reach an LLM.
No step field may hold a secret. Every step kind is a closed object, and the args and bindings grammar is closed to inputs.<name> and steps.<id>.<output>, so a literal secret cannot be embedded in the wiring.
Wiring the no-LLM seam
Behavioral determinism is the whole point. No LLM runs at Flight Plan runtime by default. If your plan needs LLM reasoning at one point, mark exactly one step kind: llm-seam and route all of it through that step. Everything else is action-call and transform, both of which hold no reference to any language-model type.
In v1 the seam is unwired by default. An llm-seam step with no configured provider is a hard error at launch, so a default launch reaches no model at all. A skill that reaches an LLM outside the marked seam fails the freeze lint and never becomes a Flight Plan. Keep the seam single. A plan that needs reasoning in more than one place routes all of it through the one marked seam or restructures to fit.
The Markdown body
Everything after the closing frontmatter is the body. It is the agentskills.io skill body. Write what the skill does, the inputs it expects, and the output it produces. The body is the prose a reader and a skill-aware host consume, and it stays a valid skill even with the Aileron block stripped.
Handoff to freeze and sign
When the skill validates and behaves the way you want, hand it to freeze. Freeze is the boundary between a skill and a Flight Plan.
aileron skill freeze weekly-metrics-digest \
--signing-key ~/.aileron/keys/author.pem \
--version 1.0.0Freeze parses and lints the manifest, rejecting any step that could reach an LLM outside the marked llm-seam. It resolves the execution environment to image digests. It builds the lockfile. It content-addresses the unit and signs it with a detached ed25519 signature. It writes the frozen version into the store as an immutable directory.
The signing key is a PEM-encoded ed25519 private key, read for signing only and never copied into the artifact. The detached signature is your attestation that the per-action trust contract is correct. See Freezing a Flight Plan for the full command and the reproducibility and signature-verification guarantees.
After freeze, launch runs the frozen Flight Plan deterministically.
Common authoring mistakes
- A literal secret in the wiring. No step field holds a value.
argsandbindingsare references (inputs.<name>orsteps.<id>.<output>), and the credential lives in the trust contract as akind/placementdeclaration, never a token. - An unmarked LLM call. Any reach to a model outside a
kind: llm-seamstep fails the freeze lint. Mark the one seam, or restructure the plan to keep the logic deterministic. - More than one seam. v1 allows exactly one marked seam. Route all reasoning through it.
- An
actionRefwith no matchingrequires.actions[].ref. A step can only call an action therequires:block declares. The freeze lint catches the mismatch. - Hand-writing the lock. The
locksection is produced by freeze. It is absent before freeze, and you never author it. - A
base64output in v1. Theencodingfield reservesbase64, but the v1 runtime materializesutf-8text only. A binary artifact waits on the deferred escape hatch: atoolstep’s mount and collect file-I/O boundary.
Where to go next
- Flight Plans. The model behind everything in this guide.
- Flight Plan Manifest Spec. The normative field reference, with the full schema and a worked example.
- Freezing a Flight Plan. Seal the skill into a signed Flight Plan version.
- Launching a Flight Plan. Run the frozen Flight Plan deterministically.
- ADR-0027: Flight Plan = Sealed Installable Skill. The design constraints behind this layer.