Skip to main content

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:

DirectiveDescription
\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 required
  • default=<value> --- sets a default value
  • schema=<json> --- sets a JSON Schema constraint (see schema below)
  • schemaRef=<string> --- references an external JSON Schema file
  • metadata=<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 for ref="<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 of actionbox run-image. This lets a single action opt into containerization at invocation time by parameterizing the ref.
  • env=<object> --- a JSON object of KEY to 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 any SIGNADOT_*) 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, and metadata. 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:

  1. Type signaling --- when a parameter has a schema, its value is written as a .json file 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.
  2. 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:

CategoryTools
Shellbash
HTTPcurl, wget
TLSca-certificates, openssl
JSON / YAMLjq, yq
Pythonpython3, pip
Text processinggrep, sed, awk
DNSdig, nslookup
TCPnetcat (nc)
gRPCgrpcurl
Version controlgit
Container runtimerunc (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:

  1. $SIGNADOT_PLAN_EXECUTION_BINDIR --- the per-execution bin/ directory under the plan-execution scratch dir, prepended at step launch. A binary placed here by one step is on the PATH of subsequent steps in the same execution regardless of whether either step runs containerized or directly in the runner.
  2. The standard Alpine locations.
  3. /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.

FieldDescription
defaultA JSON Schema applied when the plan author left the extra-input schemaless
requireWhen 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.

FieldDescription
literalA fixed image reference (e.g. "alpine:3.19"); empty means run in the runner's base image
inputNameThe name of a declared input whose value at run time is the image reference
envMap of KEY to literal env value, merged into the script's env
envFromInputMap 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:

FieldDescription
runnerGroupRunner group name
validWhether validation passed
staletrue if validation was performed at an older action or runner group revision
validatedAtValidation timestamp (RFC3339)
responseDetailed validation response

The response field contains:

  • script --- the validation script result:
    • ok --- whether the script exited successfully
    • exitCode --- the script's exit code
    • output --- 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.