Actions Developer Guide

Advanced Runtime Examples

See advanced examples for chaining APIs, WebAssembly, and secure webhooks.

Advanced Runtime Examples

GraphQL API Chaining

The api engine supports declarative GraphQL queries alongside REST endpoints, enabling seamless integration with GraphQL servers.

# actions/query_infrastructure.toml
name = "query_infrastructure"
description = "Query infrastructure state via GraphQL"

[[input]]
name = "environment"
description = "Target environment"
type = "string"
required = true

[vault_secrets.metrics_token]
collection = "observability"
secret = "metrics_api_token"

[runtime]
engine = "http"

# Native GraphQL support
[[runtime.steps]]
method = "POST"
url = "https://api.internal/graphql"
graphql_query = """
  query GetDeployments($env: String!) {
    deployments(environment: $env) {
      id
      name
      status
      lastDeployed
    }
  }
"""

[runtime.steps.graphql_variables]
env = "{{ input.environment }}"

[[runtime.steps]]
method = "POST"
url = "https://metrics.internal/graphql"
headers = { "Authorization" = "Bearer ${secrets.metrics_token}" }
graphql_query = """
  query GetMetrics($env: String!) {
    metrics(environment: $env) {
      cpu
      memory
      requests
    }
  }
"""

[runtime.steps.graphql_variables]
env = "{{ input.environment }}"

REST API Chaining with Templates

Chain multiple REST API calls with input and secret templating for orchestrated workflows.

# actions/provision_database.toml
name = "provision_database"
description = "Provision a database and configure backups via API chaining"

[[input]]
name = "db_name"
description = "Database name"
type = "string"
required = true

[[input]]
name = "instance_type"
description = "Instance type (small, medium, large)"
type = "string"
default = "medium"

[[mount]]
resource_type = "payload"
filename = "create_db_payload.json"
mount_path = "create_db_payload.json"

[vault_secrets.cloud_api_key]
collection = "cloud_credentials"
secret = "api_key"

[vault_secrets.provisioning_token]
collection = "internal_services"
secret = "provisioning_webhook_token"

[runtime]
engine = "http"

# Step 1: Create database instance
[[runtime.steps]]
method = "POST"
url = "https://api.cloud.internal/v1/databases"
headers = { "Authorization" = "Bearer ${secrets.cloud_api_key}", "Content-Type" = "application/json" }
body = "@create_db_payload.json"

# Step 2: Enable automated backups
[[runtime.steps]]
method = "POST"
url = "https://api.cloud.internal/v1/databases/{{ input.db_name }}/backups"
headers = { "Authorization" = "Bearer ${secrets.cloud_api_key}" }
body = """
{
  "schedule": "0 2 * * *",
  "retentionDays": 30,
  "encrypted": true,
  "storageClass": "standard"
}
"""

# Step 3: Notify provisioning system
[[runtime.steps]]
method = "POST"
url = "https://provisioning.internal/webhooks/db-created"
headers = { "X-Webhook-Secret" = "${secrets.provisioning_token}" }
body = """
{
  "databaseName": "{{ input.db_name }}",
  "timestamp": "{{ .now }}"
}
"""

Secure Signed Webhook Headers

The http engine supports HMAC-SHA256 signed payloads for securely triggering external CI/CD pipelines.

# actions/trigger_deployment.toml
name = "trigger_deployment"
description = "Securely trigger a deployment via signed webhook"

[[input]]
name = "version"
description = "Version to deploy"
type = "string"
required = true

[[input]]
name = "environment"
description = "Target environment"
type = "string"
required = true

[vault_secrets.github_webhook_secret]
collection = "github_credentials"
secret = "webhook_secret"

[vault_secrets.github_token]
collection = "github_credentials"
secret = "api_token"

[runtime]
engine = "http"

[[runtime.steps]]
method = "POST"
url = "https://github.com/repos/org/service/dispatches"
signature_secret = "${secrets.github_webhook_secret}"
signature_header = "X-Hub-Signature-256"
headers = { "Authorization" = "Bearer ${secrets.github_token}" }

When executing via webhook (fetch mode) directly from the Controller:

rescile-ce runner \
  --inputs version=1.2.3,environment=production \
  "http://127.0.0.1:7600/api/actions/my_module/trigger_deployment"

The runner automatically:

  1. Serializes inputs into a JSON payload
  2. Computes HMAC-SHA256 signature using the secret
  3. Sends POST request with signature header: X-Hub-Signature-256: sha256=<hex_signature>
  4. GitHub (or any compatible system) verifies the signature before processing

Tip: You can securely interact with the Controller from within your actions. When a runner is connected via an API token, the ${secrets.rescile_controller_url} and ${secrets.rescile_api_token} values are automatically injected into the action’s environment, allowing native queries back to the graph.

Dynamic Service Tunneling & Proxy Routing

A long-running service action owns its HTTP listener. Use [runtime.expose] for a runner-launched service, with protocol = "socket" (Unix) or protocol = "http" (TCP). Set auto_execute = true when service should start without a manual trigger.

# actions/start_acme_gateway.toml
name = "start_acme_gateway"
description = "ACME gateway"
run_mode = "service"
auto_execute = true
exec = [["python3", "acme_gateway.py"]]

[runtime]
engine = "nix"
flake = "./flake.nix"
files = ["./acme_gateway.py"]

[runtime.expose]
protocol = "socket"
env_var = "UNIX_SOCKET" # Application binds this allocated socket path
# For TCP instead: protocol = "http", env_var = "PORT"

Service must bind allocated UNIX_SOCKET (or PORT for TCP). A fixed TCP port may be set with port = 8080 under [runtime.expose]. Action can select its own upstream backends. In app/proxy.toml, route browser requests to action:

[[route]]
path = "/acme/*"
target = "action://cert-manager/start_acme_gateway/acme"

Controller selects one running action and forwards to its listener over runner’s existing outbound connection. Request does not start action. Keep browser links relative or configure application’s base path for /apps/cert-manager/acme/. See Action-owned service routes for tunnel limits, cookie scoping, URL rewriting, and supported headers.

WebAssembly (WASM) Execution

Execute compiled WebAssembly modules with strict sandboxing and efficient resource isolation. WASM modules must export a _start function and are executed with wasmtime.

# actions/validate_config.toml
name = "validate_config"
description = "Validate configuration using a WASM module"

[[input]]
name = "config_path"
description = "Path to configuration file"
type = "string"
required = true
default = "config.json"

[runtime]
engine = "wasi"
wasm = "./validator.wasm"
files = ["./config.json", "./schema.json"]

[[mount]]
resource_type = "config"
filename = "config.json"
mount_path = "config.json"

[[mount]]
resource_type = "schema"
filename = "schema.json"
mount_path = "schema.json"

Building WASM modules for rescile:

# Rust example with wasm-pack
wasm-pack build --target wasi --release

# The _start function serves as the entry point
#[no_mangle]
pub extern "C" fn _start() {
    // Read from /workspace/config.json
    // Validate against /workspace/schema.json
    // Exit with status 0 on success, 1 on failure
}

WASM execution provides:

  • Zero external dependencies: No syscall overhead
  • Strict I/O isolation: Only explicit mounts are accessible
  • High performance: Near-native execution speed
  • Deterministic behavior: Same code always produces same output

Combining Engines: Multi-Stage Pipelines

Complex workflows can chain multiple engines in sequence:

# actions/deploy_with_validation.toml
name = "deploy_with_validation"
description = "Validate config (WASM) → Query state (GraphQL) → Deploy (Nix)"

[[input]]
name = "version"
type = "string"
required = true

# Stage 1: Native WASM Engine
[[stages]]
name = "validate"
[stages.runtime]
engine = "wasi"
wasm = "./validator.wasm"
args = ["{{ input.version }}"]

# Stage 2: Native API Engine
[[stages]]
name = "query_state"
[stages.runtime]
engine = "http"
[[stages.runtime.steps]]
method = "POST"
url = "https://api.internal/graphql"
graphql_query = """
  query GetState($version: String!) {
    state(version: $version) {
      status
    }
  }
"""
[stages.runtime.steps.graphql_variables]
version = "{{ input.version }}"

# Stage 3: Native Nix Engine
[[stages]]
name = "deploy"
[stages.runtime]
engine = "nix"
flake = "./flake.nix"
exec = [
    ["nix", "flake", "update"],
    ["nix", "develop", "--command", "deploy.sh", "{{ input.version }}"]
]

Scheduled Actions (Calendar Triggers)

A schedule makes the controller queue the action at every due instant. Combine it with action expansion to derive per-resource calendars from graph data — each application below carries its own backup_schedule property, so one action file produces two independent schedule bindings.

# actions/backup.toml
name = "backup_{{ origin_resource.name }}"
description = "Nightly backup for {{ origin_resource.name }}"
origin_resource = "application"

# Cron comes from the application row; timezone from a module parameter.
schedule = { cron = "{{ origin_resource.backup_schedule }}", timezone = "{{ params.backup_timezone }}" }

exec = [["python3", "backup.py", "{{ origin_resource.name }}"]]

[runtime]
engine = "nix"
flake = "./flake.nix"
files = ["./backup.py"]
# assets/application.csv
name,backup_schedule
billing,30 2 * * *
catalog,0 4 * * SUN

The controller validates each rendered schedule after the graph build. A bad expression (such as a missing backup_schedule property) disables only that binding and surfaces a diagnostic on GET /api/schedules — the rest of the workspace keeps building and serving.

Fixed schedules work without any graph data:

# actions/rotate_keys.toml
name = "rotate_keys"
description = "Rotate API keys on the last Friday of the month"
schedule = "0 3 * * 5#L"          # last Friday, 03:30 UTC
# schedule = { cron = "0 3 * * 5#L", timezone = "Europe/Zurich" } — with explicit timezone
exec = [["python3", "rotate.py"]]

[runtime]
engine = "api"

[[runtime.steps]]
method = "POST"
url = "https://api.internal/v1/keys/rotate"

See Scheduled Actions for pattern syntax, missed-occurrence and overlap semantics, and observability.