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 }}"]
]