API Exposure Guide

Custom API Contracts

Define intent-based RPC endpoints using contracts to apply complex mutations safely.

Custom API Contracts

As module developers, you often need to expose intent-based operations that mutate multiple underlying assets simultaneously. Instead of forcing external consumers or front-end applications to understand your internal CSV schema and graph relationships, you can define Custom API Contracts.

Contracts provide strict validation boundaries and atomic execution. They encapsulate complex intents (like ProvisionVM or DecommissionService) into simple, high-level RPC endpoints.

Contract Definition

Contracts are defined as .toml files placed in the contracts/ directory of your module. A contract parses incoming JSON payloads against an [[input]] schema, and applies mutations via sequential [[write]] blocks.

Under the hood, these mutations leverage the Tera templating engine, allowing you to dynamically map inputs to target records.

# modules/my_module/contracts/provision_vm.toml
name = "ProvisionVM"
description = "Provisions a VM and allocates an IP simultaneously."

# Input Validation Schema
[[input]]
name = "vm_name"
type = "string"
required = true

[[input]]
name = "environment"
type = "string"
required = false
allowed_values = ["dev", "prod"]

# Atomic Action 1: Add VM to the desired state asset
[[write]]
target_asset = "desired_vms"
action = "add" 
record = { name = "{{ input.vm_name }}", status = "planned", env = "{{ input.environment | default(value='dev') }}" }

# Atomic Action 2: Allocate IP (upsert example)
[[write]]
target_asset = "ip_allocations"
action = "upsert"
# Matches existing rows by 'assigned_device'. If missing, creates it.
lookup_by = { assigned_device = "{{ input.vm_name }}" }
record = { name = "ip-{{ input.vm_name }}", status = "allocated" }

Input Schema Definition

The [[input]] blocks enforce strict validation on the incoming JSON payload before any mutation occurs.

  • name: The JSON key expected in the payload.
  • type: The expected data type (string, integer, number, boolean).
  • required: Boolean indicating if the payload must contain this key.
  • allowed_values: An optional array of strings defining an enum of acceptable values.

Execution Lifecycle and Atomicity

To guarantee safe and idempotent state mutations, the Rescile controller processes contracts with transactional guarantees:

  1. Validation: The JSON payload is rigorously validated against the defined [[input]] blocks.
  2. Lock Acquisition: A global asynchronous lock (ASSET_WRITE_LOCK) is acquired to prevent race conditions from concurrent RPC calls.
  3. In-Memory Transaction: The engine evaluates all [[write]] instructions sequentially against an in-memory representation of the CSV assets.
  4. Pre-condition Guards: match_on arrays act as conditional guards. Evaluated against the input payload, they determine if a specific [[write]] block should execute.
  5. Rollback on Error: If any write block fails (e.g., attempting to add a primary key that already exists, or failing to find a record during a change action), the entire execution aborts instantly. No partial data is written to disk.
  6. Flush & Rebuild: If all mutations succeed, the updated CSV assets are flushed to disk, and a background graph rebuild is triggered automatically to reflect the new state.

Modifying Assets: [[write]] Blocks

The [[write]] block is where state mutations happen. You specify the target_asset (the name of the CSV file, without the .csv extension), the action, and the payload.

Available actions:

  • add: Inserts a new row. Fails if the primary key (name) already exists.
  • change: Updates an existing row. Fails if the target row cannot be found.
  • delete: Removes a row. Silently skips if the row does not exist, ensuring idempotency.
  • upsert: Updates the row if it matches criteria, or inserts a new row if it does not.

To execute these actions precisely, contracts utilize two distinct mechanisms: lookup_by for row targeting, and match_on for precondition guards.

lookup_by: Row Targeting

For change, delete, and upsert actions, the engine must identify exactly which row in the CSV asset to mutate. lookup_by defines this lookup strategy.

Map Syntax (Multi-Column Targeting): You can target specific columns by mapping headers to Tera templates. All conditions must match exactly in the CSV.

[[write]]
target_asset = "network_interfaces"
action = "change"
lookup_by = { mac_address = "{{ input.mac }}", vlan = "{{ input.vlan }}" }
record = { status = "active" }

String Syntax (Primary Key Targeting): Passing a single string is syntactic sugar for { name = "<rendered_string>" }.

[[write]]
target_asset = "network_interfaces"
action = "delete"
lookup_by = "{{ input.interface_id }}"

match_on: Pre-condition Guards

While lookup_by finds the row in the CSV, match_on acts as a conditional guard that evaluates whether the [[write]] block should execute at all.

It evaluates standard Rescile MatchRule conditions against the incoming payload (accessed via the input property). If the conditions evaluate to false, the write block is bypassed seamlessly without causing an error.

Example: Conditional Execution

# Only adds the logging agent if the environment is set to 'prod'
[[write]]
target_asset = "vm_agents"
action = "add"
match_on = [
  { property = "input.environment", value = "prod" }
]
record = { name = "agent-{{ input.vm_name }}", type = "logger" }

Consuming Contracts

Once defined in your module, the Rescile controller automatically parses your Contracts and dynamically injects them into both the REST and GraphQL APIs.

GraphQL API

Contracts are exposed as root mutations in the GraphQL schema, strictly typed according to your [[input]] definitions.

mutation Provision {
  ProvisionVM(input: { vm_name: "db-node-01", environment: "prod" }) {
    success
    message
    affected_assets
  }
}

REST API

Contracts can be executed directly via HTTP POST, passing the payload as standard JSON.

Endpoint: POST /api/contracts/:contract_name

POST /api/contracts/ProvisionVM
Content-Type: application/json

{
  "vm_name": "db-node-01",
  "environment": "prod"
}

Response:

{
  "success": true,
  "message": "Contract executed successfully",
  "affected_assets": [
    "desired_vms.csv",
    "ip_allocations.csv"
  ]
}