AileronControlPlane

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.

  • credential declares the credential kind (none, api-key, oauth2, aws-sigv4) and its placement (header, query, cookie, body, signing, session). It never carries a credential value. An oauth2 credential is always placed in a header; an aws-sigv4 credential is always placed via signing.
  • oauth is required when credential.kind is oauth2. It declares the scopes, endpoints, and refresh behavior, never a token.
  • hosts is the closed list of upstream hosts the action reaches, as host:port pairs. The declared host set IS the security boundary. The runtime grants nothing undeclared. paths optionally narrows the request paths on those hosts.
  • effect is one of read, write, delete, spend, external-send. It drives default approval routing. A read runs unattended; the other four raise an approval gate at launch.
  • idempotency.safeToRetry declares whether re-running with the same inputs produces no additional effect. idempotency.idempotencyKey declares whether the operation accepts a client-supplied key. Read ops are typically safe to retry; writes that mutate the world are not.
  • redaction optionally lists rules applied to response fields before they reach the agent or the author. A drop rule removes a field, a mask rule replaces its value, and a hash rule replaces it with a stable hash.
  • verification optionally declares a read-only probe the runtime can call to confirm the credential still works.
  • audit.fields declares which of the closed audit-record fields this operation emits. The closed set is connector-hash, action-manifest-version, credential-binding, identity-label, approved-input, approval-decision, network-target, operation-effect, request-summary, response-summary, and result.

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.tools names 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, and 2.19.1 all validate). Freeze resolves each entry to its catalog devcontainer Feature and composes one image on the Aileron-provided runner base.
  • environment.image names a custom base image, the escape hatch when the curated catalog does not carry the tooling you need. Freeze resolves a tag to an image@sha256: digest pin. Declaring both tools and image composes 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.

  • name is the input name, unique within the manifest.
  • type is one of string, number, boolean, timestamp, object, array.
  • description is optional human-readable semantics.
  • resolution is the resolution rule, discriminated by its rule field.

There are three resolution rules.

  • literal takes a value passed at launch. An optional default applies when no value is passed.
  • dynamic resolves a launch-relative value once at launch. Its value is now (the launch timestamp) or today (the launch date).
  • source reads from a live source. Its source.actionRef names the action whose result resolves the input, with an optional source.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.

  • name is the artifact name, unique within the manifest. The audit references artifacts by this name.
  • mimeType is the artifact media type.
  • encoding is utf-8 or base64. v1 implements utf-8 only. base64 is reserved.
  • publish.target is file or none. When target is file, publish.path names 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.

kindReaches an LLMSemantics
action-callNoInvokes a declared action. Its actionRef names the action and its args bind the action’s arguments.
transformNoRuns deterministic no-LLM logic over data already in the graph. It has no host, network, or credential surface.
toolNoRuns 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-seamYesThe 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-call step’s actionRef must match a declared requires.actions[].ref.
  • An llm-seam is 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.0

Freeze 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. args and bindings are references (inputs.<name> or steps.<id>.<output>), and the credential lives in the trust contract as a kind/placement declaration, never a token.
  • An unmarked LLM call. Any reach to a model outside a kind: llm-seam step 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 actionRef with no matching requires.actions[].ref. A step can only call an action the requires: block declares. The freeze lint catches the mismatch.
  • Hand-writing the lock. The lock section is produced by freeze. It is absent before freeze, and you never author it.
  • A base64 output in v1. The encoding field reserves base64, but the v1 runtime materializes utf-8 text only. A binary artifact waits on the deferred escape hatch: a tool step’s mount and collect file-I/O boundary.

Where to go next