Documentation

Actions Developer Guide

Learn how to define securely sandboxed, trigger-based actions and expose them to the UI or AI Agents via the MCP Server.

Air-Gap Ready Self-Hosted Open Ecosystem

Actions Developer Guide

Declarative Module Actions (The “Act” Phase)

The Actions engine in rescile provides a standardized, secure execution environment for infrastructure changes and business logic. Actions natively expose Write/Execute APIs to both the Frontend UI and AI Agents via the Model Context Protocol (MCP).

Actions use a trigger-based execution model. Instead of handling raw script files directly, clients interact with standardized Action APIs. The rescile engine manages the action definitions, securely sandboxes the execution, and seamlessly binds contextual data from the graph, such as Vault secrets and generated artifacts.


Directory Structure

Actions are defined within your module’s ecosystem. While models mutate the graph and outputs generate files, actions define executable logic.

Action definitions are placed in the actions/ directory, while the actual execution binaries or scripts live in the runtimes/ directory.

my-module/
├── module.toml
├── models/
├── output/             
├── actions/            <-- Declarative action definitions (.toml)
│   ├── remediate.toml
│   └── deploy_aws_hub.toml
└── runtimes/           <-- Executable scripts or binaries (WASM, JS, Py, Nix)
    └── flake.nix       <-- Reproducible environment definition

Defining an Action

An action TOML file binds a specific runtime to an input schema, a graph context, and secure credentials. Actions can either be Singleton Actions (one logical action) or Dynamic Actions (expanded across graph nodes).

Singleton Actions

Singleton actions execute once independently of the graph. Below is an example of a singleton action that uses the reproducible Nix runtime to execute Python deployment scripts (e.g., AWS Network Hub).

# actions/deploy_aws_hub.toml
name = "deploy_aws_hub"
description = "Orchestrates the deployment of the AWS Zurich Transit VPC and Ingress Filters"
run_mode = "job" # Optional: Can be "job" (default) or "service"

exec = [
    ["sh", "-c", "if [ '{{ input.dry_run }}' = 'true' ]; then python3 orchestrator.py --dry-run; else python3 orchestrator.py; fi"]
]

[[input]]
name = "dry_run"
description = "Check state and log intended actions without calling AWS"
type = "boolean" # default string

# You can restrict inputs to a predefined list of allowed values.
# The UI will automatically render a dropdown for these options.
[[input]]
name = "environment"
description = "Target deployment environment"
type = "string"
allowed_values = ["dev", "staging", "prod"]
default = "dev"
required = true

# You can also dynamically link an input to a specific graph resource type. 
# This tells the UI to automatically query the graph and render a dropdown 
# containing valid nodes that currently exist!
[[input]]
name = "target_vpc"
description = "The VPC to deploy into"
type = "string"
resource_link = "aws_vpc.name"

[runtime]
engine = "nix"
flake = "./flake.nix"
files = ["./orchestrator.py", "./state_manager.py"]

# Mount the expected Python module files from the graph context.
[[mount]]
resource_type = "python_module"
filename = "zurich_transit_vpc.py"
mount_path = "module/zurich_transit_vpc.py"

Note: origin_resource must be defined at the action top level. It is no longer accepted inside [[mount]] blocks. Mounts are resolved by the rendered filename alone.


Dynamic Actions (Action Expansion)

Dynamic actions allow you to treat an [[action]] block as an Action Template. By introducing origin_resource and optional match_on filters, the importer will iterate over the graph, evaluate the template, and expand it into multiple concrete actions—one for each matching node in the graph.

This is highly useful for defining actions that target specific hosts, cloud providers, or network segments across your infrastructure without manually creating hundreds of action files.

The following example defines an action template that expands for each host resource where os_family is Linux and status is active.

# actions/patch_os.toml
# 1. Target a specific resource type in the graph
origin_resource = "host"

# 2. Optionally filter which hosts get this action
match_on = [
  { property = "os_family", value = "linux" },
  { property = "status", value = "active" }
]

# 3. Template the action name to make it unique per resource
name = "patch-os-{{ origin_resource.name }}"
description = "Install OS updates on {{ origin_resource.name }}"
run_mode = "agent"

# 4. Dynamically generate capabilities/constraints based on the node's properties!
constraints = [
  "cloud_provider:{{ origin_resource.cloud_provider }}", 
  "network_segment:{{ origin_resource.network_segment }}",
  "hostname:{{ origin_resource.hostname }}"
]

[runtime]
engine = "system"
exec = [["apt-get", "update", "-y"]]

# 5. Inject node data into the environment variables
[env]
TARGET_IP = "{{ origin_resource.ip_address }}"

How Dynamic Actions Work

  1. Compile Time: The graph properties (name, description, constraints) are templated dynamically during the import phase. The Controller expands the single template into uniquely named actions (e.g., patch-os-web-01, patch-os-db-02) based on the graph data.
  2. Dispatch Time: The dispatcher automatically queues the specific actions independently.
  3. Execution Time: The dynamic capabilities map exactly to agents running in the targeted environments. If an agent runner reports capabilities: {"cloud_provider:aws": true, "network_segment:dmz": true, "hostname:web-01": true}, it’s a perfect match, ensuring the action runs exactly where it’s supposed to (e.g., inside the VPC of the specific cloud provider).