Action Specification
Overview
An Action defines an atomic operation that can be invoked by plans. Actions contain their implementation as markdown with embedded code blocks, and declare their inputs and outputs via inline directives.
Actions are checked into the
signadot/actions repository
and synced from git into your org's action registry.
Here is an example:
id: act_abc123
name: run-playwright
spec:
body: |
\description{"Run a Playwright test and capture the report."}
\image{"mcr.microsoft.com/playwright:v1.59.1", env={"NODE_ENV":"test"}}
\timeout{"5m"}
Run \input{script, required} as a Playwright test.
\output{exitCode, schema={"type": "integer"}}
\output{report, schema={"type": "object"}}
```sh
npx playwright test "$(cat ./context/script)" \
--reporter=json > ./outputs/report
printf '%d' "$?" > ./outputs/exitCode
```
published: true
status:
description: Run a Playwright test and capture the report.
bodyParams:
- name: script
required: true
bodyOutputs:
- name: exitCode
schema: {"type": "integer"}
- name: report
schema: {"type": "object"}
bodyImage:
literal: mcr.microsoft.com/playwright:v1.59.1
env:
NODE_ENV: test
bodyTimeout: 5m
createdAt: "2026-01-15T10:00:00Z"
updatedAt: "2026-01-15T10:00:00Z"
revision: 1
id
The id is a server-assigned immutable identifier for the action.
name
The name is a mutable, unique name for the action within the
organization.
spec
The action spec contains the user-editable specification.
spec.body
The body is the action implementation as markdown with code blocks.
Inputs, outputs, and other directives are declared inline using the
following syntax:
| Directive | Description |
|---|---|
\input{name, ...} | Declares an input parameter |
\output{name, ...} | Declares an output field |
\description{"..."} | Short one-line description |
\extra_inputs_schema{...} | Policy for schemas on plan-author-introduced extraInputs |
\image{...} | Run the action's shell fenced block inside an OCI image via actionbox run-image, and/or pass env vars to the script |
\timeout{...} | Wall-clock timeout for the step's script |
Code blocks in the body contain the executable implementation.
Shell code blocks (empty, sh, or bash info string) are
concatenated into the script that the runner executes. Validation
scripts use validation fenced code blocks and are extracted
into status.validationScript by the server. Other code block
types (e.g. json, yaml) are treated as documentation.
Directives inside fenced code blocks are ignored --- only directives
in prose are parsed. To include a literal \input{ or \output{
in prose without it being parsed as a directive, escape the opening
brace: \input\{.
\input and \output syntax
\input{name}
\input{name, required}
\input{name, key=value, ...}
The first argument is the field name, which must be an identifier
([a-zA-Z_][a-zA-Z0-9_]*). Subsequent comma-separated options are
either bare keywords or key=value pairs.
Supported modifiers:
required--- marks the field as requireddefault=<value>--- sets a default valueschema=<json>--- sets a JSON Schema constraint (see schema below)schemaRef=<string>--- references an external JSON Schema filemetadata=<json>--- sets metadata key-value pairs (e.g.contentType="text/html")
Any unrecognized key="string" pair is treated as a metadata entry.
\description syntax
\description{"Short description of the action"}
The value must be a double-quoted string. At most one \description
directive is allowed per action. Maximum length is 100 characters.
\extra_inputs_schema syntax
\extra_inputs_schema{default=<JSON Schema>}
\extra_inputs_schema{require}
Declares the action author's policy for how plan creation should
resolve missing schemas on a step's
extraInputs. At most one
\extra_inputs_schema directive is allowed per action, and exactly
one primitive must be set:
default=<schema>--- when the plan author leaves an extra-input schemaless, fill it in with this fallback schema. Plan-author-declared schemas always win.require--- plan creation fails if any extra-input on a step of this action is schemaless. No fallback is applied; plan authors must be explicit.
The policy applies to both compile-from-prompt and hand-created plans.
Example:
\extra_inputs_schema{default={}}
The {} schema means "accept any JSON value" --- a permissive default
that still routes the input through the JSON loader so structured
values stay typed when the action consumes them.
\image syntax
\image{"alpine:3.19"}
\image{ref="alpine:3.19"}
\image{ref={"input":"NAME"}}
\image{ref="alpine:3.19", env={"DEBUG":"1","TOKEN":{"input":"apiToken"}}}
\image{env={"FOO":"bar"}}
Declares that the action's shell fenced block runs inside an OCI image
via actionbox run-image rather than directly in the runner's
process, and/or sets env vars for the script. At most one \image
directive is allowed per action. The directive accepts comma-separated
arguments:
- A leading positional
"<reference>"is shorthand forref="<reference>". ref=<value>--- the image reference, either as a quoted literal or as an input ref{"input":"NAME"}resolved at run time. An empty literal (\image{""},\image{ref=""}, or an input ref that resolves to"") routes the action through the shell runner in the runner's base image instead ofactionbox run-image. This lets a single action opt into containerization at invocation time by parameterizing the ref.env=<object>--- a JSON object ofKEYto value, where each value is either a quoted literal or{"input":"NAME"}. Keys must match the POSIX env-name grammar ([A-Za-z_][A-Za-z0-9_]*). The entries are merged into the action's process env in either dispatch path (containerized or shell). Reserved runner-set keys (HOME,TMPDIR,PATH,SSL_CERT_FILE, and anySIGNADOT_*) are silently filtered so the runner's contract values cannot be overridden.
When ref is a literal, the reference is validated against the
OCI/Docker reference grammar at compile time. The platform's
run-image action uses ref={"input":"image"} so a single action can
run any image the caller chooses.
Image-using actions still execute the action body's shell fenced block
as their entrypoint; the image only changes where the script runs.
Inputs and outputs continue to flow through ./context/ and
./outputs/ as for other actions. The
SIGNADOT_PLAN_EXECUTION_BINDIR directory is on PATH in
both paths, so a binary dropped by step N is callable by step N+1
regardless of which dispatch path either step uses.
\timeout syntax
\timeout{"60s"}
\timeout{{"input":"NAME"}}
Sets a wall-clock timeout on the step's script. At most one \timeout
directive is allowed per action, and it applies in either dispatch
path (containerized or shell). Two forms are supported:
- Literal: a quoted duration string parsed by Go's
time.ParseDuration(e.g."60s","5m","1h30m"). - From input: a single-key sentinel
{"input":"NAME"}whose value at run time is the duration string. The double{{is not shorthand --- the outer{opens the directive body and the inner{opens the input-ref object. An empty resolved value falls through to the runner's system default cap.
The directive's literal duration is validated at compile time. The runner layers a system-default cap on top, so a too-large value is clamped at dispatch.
Value syntax
Directive values follow these rules:
- Strings are double-quoted:
"hello". Within a string,\"produces a literal"and\\produces a literal\. No other escape sequences are recognized. - JSON objects and JSON arrays are supported as values for
default,schema, andmetadata. Nested structures and strings within JSON are handled correctly. - Bare keywords (like
required) have no value.
Schema
The schema modifier accepts a JSON Schema object using the
JSON Schema vocabulary (draft-04 and
later drafts are accepted). The schema is stored as-is and serves
two purposes:
- Type signaling --- when a parameter has a schema, its value is
written as a
.jsonfile to the action's execution context with the raw JSON bytes preserved verbatim. Without a schema, JSON string values are unquoted (outer quotes stripped) so the action receives plain text. See JSON value handling for details. - Documentation --- the schema communicates the expected type and constraints to plan authors and the LLM compiler.
Examples:
\input{count, required, schema={"type":"integer","minimum":1}}
\input{config, schema={"type":"object","properties":{"retries":{"type":"integer"}}}}
\output{response, schema={"type":"object"}}
A schemaRef modifier is also supported for actions checked into
signadot/actions and synced
from git. It references an external JSON Schema file relative to the
action directory (e.g. schemaRef="schemas/response.json").
The file contents are read and inlined into the schema field at
sync time.
body is optional.
spec.published
Controls whether this action is visible and usable in plan compilation.
published is required.
Runtime environment
By default, action code blocks execute inside the plan runtime container, an Alpine Linux-based image with the following tools pre-installed:
| Category | Tools |
|---|---|
| Shell | bash |
| HTTP | curl, wget |
| TLS | ca-certificates, openssl |
| JSON / YAML | jq, yq |
| Python | python3, pip |
| Text processing | grep, sed, awk |
| DNS | dig, nslookup |
| TCP | netcat (nc) |
| gRPC | grpcurl |
| Version control | git |
| Container runtime | runc (used by actionbox run-image) |
Actions that need tools beyond the default set declare an
\image{...} directive with a non-empty ref. The action's
shell fenced block then runs inside that OCI image via actionbox run-image instead of the default runtime container. Inputs and
outputs continue to flow through ./context/ and ./outputs/, and
the per-execution
SIGNADOT_PLAN_EXECUTION_BINDIR is on PATH so binaries
staged by upstream steps remain callable.
PATH
The runner builds PATH so that per-execution binaries take
precedence over the runtime image's tools:
$SIGNADOT_PLAN_EXECUTION_BINDIR--- the per-executionbin/directory under the plan-execution scratch dir, prepended at step launch. A binary placed here by one step is on thePATHof subsequent steps in the same execution regardless of whether either step runs containerized or directly in the runner.- The standard Alpine locations.
/signadot/shared/bin/--- platform binaries installed by the runner's init container.
All the tools listed above are discoverable via PATH during both
validation and script execution.
User
Actions run as a non-root user (signadot, UID/GID 3377). The
container's root filesystem is read-only.
Write isolation
Each step executes in its own working directory under the plan's
per-execution directory. Input parameters are written to a
context/ subdirectory and outputs are collected from an outputs/
subdirectory.
When write isolation is enabled, each step runs in a private mount
namespace where the plan's per-execution directory is writable and
everything outside it (including the rest of the shared platform
volume, and other executions' directories) is remounted read-only.
The per-execution directory contains every step's workdir plus the
plan-execution scratch dir
(SIGNADOT_PLAN_EXECUTION_SCRATCHDIR)
and its bin/ subdir
(SIGNADOT_PLAN_EXECUTION_BINDIR),
so a step can stage a binary or file for downstream steps in the same
execution to pick up. The scratch dir is also bind-mounted read-write
into containerized steps via actionbox run-image, so cross-step
sharing works the same way regardless of dispatch path. Per-step
isolation is by working-directory convention --- it is not enforced
by the mount namespace --- but cross-execution isolation is
enforced.
A per-step tmp/ directory is provided via TMPDIR.
When write isolation is disabled, steps share the container's filesystem and are isolated only by working-directory convention, including across executions running on the same runner.
status
The action status is server-managed.
status.description
A short one-line description parsed from the \description{...}
directive in the body.
status.bodyParams
An array of field objects parsed from
\input{...} directives in the body. These define the inputs the
action accepts.
status.bodyOutputs
An array of field objects parsed from
\output{...} directives in the body. These define the outputs the
action produces.
status.extraInputsSchemaPolicy
The action author's declared policy for how plan creation treats
schemaless extraInputs on
a step using this action. Parsed from the
\extra_inputs_schema{...} directive in the
body, or null when no directive was declared.
| Field | Description |
|---|---|
default | A JSON Schema applied when the plan author left the extra-input schemaless |
require | When true, plan creation fails if any extra-input on a step of this action is schemaless |
At most one of default or require is set.
status.bodyImage
The parsed \image{...} directive, or null when no
directive was declared. When literal (or the resolved input value)
is non-empty, plans using this action will execute the action's shell
fenced block inside the named OCI image via actionbox run-image. An
empty image ref means the action runs in the runner's base image with
the directive's env still applied.
| Field | Description |
|---|---|
literal | A fixed image reference (e.g. "alpine:3.19"); empty means run in the runner's base image |
inputName | The name of a declared input whose value at run time is the image reference |
env | Map of KEY to literal env value, merged into the script's env |
envFromInput | Map of KEY to input name; the input's value at run time is the env value |
At most one of literal or inputName is set. Reserved runner-set
keys (HOME, TMPDIR, PATH, SSL_CERT_FILE, SIGNADOT_*) are
silently filtered from env and envFromInput so they cannot
override the runner's contract values.
status.bodyTimeout
The literal duration string parsed from a \timeout{"..."}
directive, or empty when the action declared no timeout (or used the
input-ref form). Mutually exclusive with status.bodyTimeoutInputName.
status.bodyTimeoutInputName
The name of a declared input whose value at run time is the duration
string, parsed from \timeout{{"input":"NAME"}}. Empty when the
action declared no timeout (or used the literal form). Mutually
exclusive with status.bodyTimeout.
status.validationScript
The shell script extracted from validation code blocks in
the body. This script is run on runner groups to verify that the
runner environment satisfies the action's requirements.
status.validations
An array of per-runner-group validation results.
Each entry has the following fields:
| Field | Description |
|---|---|
runnerGroup | Runner group name |
valid | Whether validation passed |
stale | true if validation was performed at an older action or runner group revision |
validatedAt | Validation timestamp (RFC3339) |
response | Detailed validation response |
The response field contains:
script--- the validation script result:ok--- whether the script exited successfullyexitCode--- the script's exit codeoutput--- combined stdout/stderr
status.createdAt
When the action was created. Serialized as RFC3339.
status.updatedAt
When the action was last updated. Serialized as RFC3339.
status.revision
The action's current revision number. Incremented on each update.