Functions: code behind your airdress
A function is TypeScript or JavaScript that runs behind your airdress: a webhook relay, a counter, a bridge to something at home. You write it in a directory, sign it, and deploy it; your airdress bundles and runs it. The checks are the airdress’s own, and the CLI prints what it answers.
airdress functions and airdress fn are the same command.
A function directory
Section titled “A function directory”hello/ function.json # id, runtime, capabilities, the SDK version it pins function.yaml # the manifest a deploy applies: grants, config, events src/ main.tsThe tree that is published is function.json and every regular file
under src/, and nothing else. function.yaml, a README or a
tsconfig.json beside them stay local. Symlinks are refused.
Start from a template:
airdress fn templates # what you can start fromairdress fn new hello ./hello # writes the tree and a function.yamlairdress fn new hello ./hello --function-id acme.helloA function id has two dotted labels. Without --function-id, a dotted
directory name is used as is; any other name becomes local.<name>.
Sign, then deploy
Section titled “Sign, then deploy”Source is signed with an Ed25519 key you hold. Make one once:
airdress fn keygen --out ~/.airdress/function-signing.key # 0600; prints the public key and fingerprintPass it with --signing-key <file>, or put the hex seed in
AIRDRESS_FUNCTION_SIGNING_KEY. The seed itself is never printed.
airdress fn deploy ./hello --signing-key ~/.airdress/function-signing.keyairdress fn deploy ./hello --plan # check, compare, dry-run; writes nothingairdress fn deploy ./hello --yes # scripts: confirm without askingdeploy checks the tree, compares the digest your airdress computed with
its own, asks once, signs and publishes. Then:
- for a function that already exists, it promotes the new version,
which changes
spec.source.versionand nothing else, so a deploy can never widen a grant, change the config or touch who may sign; - for a new function, it applies one manifest: the directory’s
function.yaml, or one drafted fromfunction.json, with your key as the signer. The manifest is shown in full before it is sent and written back asfunction.yaml.
It then waits until the function reports the new version loaded. The first
function after an airdress update compiles the engine, which takes about
15 seconds; --wait-timeout adjusts the wait.
Who may sign is the function’s signer set. The CLI checks membership
first; signer_not_this_client lists the members when yours is not one.
airdress fn signers add hello --machine <machine id> # also updates the committed function.yamlairdress fn signers remove hello --key <hex>Validate, publish, promote
Section titled “Validate, publish, promote”The steps deploy combines are commands of their own:
airdress fn validate ./hello --name hello # every check a publish runs; stores nothingairdress fn publish ./hello --name hello --signing-key seed.hex --based-on sha256:<running>airdress fn promote hello sha256:<v> --based-on sha256:<running> # run any stored versionairdress fn promote hello sha256:<v> --dry-runRefusals print one per line as path:line:column: Reason: message, plus
one line per capability the owner has not granted, naming its
spec.capabilities path. Any refusal exits non-zero.
A stale base is never retried. Once a function serves a version, a
publish or promote must name it with --based-on. If the function moved
in between, both versions are printed and the command exits 6. That is
how two writers, an editor and a pipeline, stay honest with each other.
Read what runs
Section titled “Read what runs”airdress fn versions hello # what it served, newest first, and what is storedairdress fn source sha256:<v> # a version's manifest and file indexairdress fn source sha256:<v> src/main.ts # one file, rawairdress fn logs hello --since 15m --follow # the durable log; --invocation <id> for one callThe SDK, @airdress/functions
Section titled “The SDK, @airdress/functions”A function imports the library as @airdress/functions/<module> and pins
an exact version in function.json ("sdk": "1.0.0"). Your airdress
bundles its own compiled-in copy at publish. Everything the CLI writes for
the SDK sits beside function.json, never under src/, so none of it is
published.
airdress fn sdk pull ./hello # types and a local copy for the pinned versionairdress fn sdk pull ./hello --pin newest # (re)pin first: `newest` or an exact versionairdress fn sdk vendor ./hello # copy the modules into src/sdk/, drop the pinnode test/main.test.ts # or bun, or deno run --node-modules-dir=manual --allow-readThe copy comes from your airdress, never from npm, and lands in
.airdress/node_modules/@airdress/functions/. pull links node_modules
to it at the function root so Node, Bun and Deno resolve the bare import,
and writes a package.json with "type": "module" when there is none.
.airdress/sdk-<version>.d.ts declares every module and the airdress
global; the generated tsconfig.json includes it. Your airdress does not
type-check; the editor and tsc --noEmit do.
An SDK refusal (SdkNotPinned, SdkVersionUnknown, SdkModuleUnknown,
SdkCapabilityNotRequested, …) prints at the import with a fix: line.
Notes, such as a deprecated module or a warning about concurrency, print
as information and never stop a deploy.
Deploying from CI
Section titled “Deploying from CI”A repository holds function directories, each with its function.yaml,
and optionally airdress.functions.yaml at its root:
layout: 1operator: <your airdress address>functions: - path: functions/relay - path: functions/digest manifest: deploy/prod/digest.yamlairdress fn layout-schema prints its JSON Schema. Without the file, every
directory holding a js-source/v1 function.json and a function.yaml is
a function.
airdress fn deploy --ci --since "$BEFORE_SHA" # or --allCI mode never prompts, never creates a function and never applies. It
deploys the functions whose function.json or src/ changed, reads
basedOn and the signer set from the manifest at the branch head, and after
the promote rewrites exactly one scalar, spec.source.version, in the
manifest for the pipeline to commit. A stale base names who deployed what
runs now, and is never retried. --output json prints one object per
function.
The runner authenticates as an approved machine, with no account profile. Secrets come from files or from environment variables holding their contents, never from arguments:
| What | Where |
|---|---|
| machine key | --machine-key <file> or AIRDRESS_MACHINE_KEY (path or contents) |
| its enrollment record | <key>.json beside it, or AIRDRESS_MACHINE_ENROLLMENT (path or JSON) |
| the airdress | --operator-url or AIRDRESS_OPERATOR_URL (required with a machine key) |
| source-signing seed | --signing-key <file> or AIRDRESS_FUNCTION_SIGNING_KEY |
One-time setup, by the owner: enroll and approve the machine
(machines), register its source-signing
key on the airdress, grant it Publish, Promote and ReadStatus on each
function, then airdress fn signers add <name> --machine <id> and commit
the updated function.yaml. The CLI warns when the machine’s approval
lapses within 14 days and stops with machine_authorization_expired once
it has.
The public GitHub Action
airdress-co/deploy-functions
wraps this mode.
Related
Section titled “Related”- Install the CLI and get started.
- Claude Code: the same dev loop (validate, deploy, promote, logs) as tools in your editor.
- Exit codes:
6means a stale base, and is never retried. - Resources: a function is a resource of kind
Function.