Authoring Guide

Design Patterns

Field-tested design and implementation patterns for building rescile modules, gateways, actions, and dashboards.

Rescile Design Patterns

A curated catalog of patterns proven in production modules. Each pattern links to the detailed reference pages. Treat these as defaults; deviate consciously.


1. Data & Schema

CSV assets as ground truth

All state lives in data/assets/*.csv; first column = primary key (name). GraphQL schema, relations, outputs, and actions are derived from these files, and runtime mutations are persisted back into them. Declare every column in module.toml. → Asset Management

Convention columns + [[retype_relation]] for foreign keys

Name a column after the target asset file (e.g. certificate) — the importer auto-creates the relation (multi-value aware); [[retype_relation]] gives it a semantic type (SERVES_CERT). This survives mutation-triggered rebuilds. Avoid _-prefixed columns for values you later want to join on (the stripped property becomes an unstable relation). → Asset Management — Creating Relationships, Retyping Relations

join for exact keys, match_on for predicates, match_with for Tera expressions (is containing(...) for multi-value cells, is ending_with(...) for suffixes — Tera’s in operator is not supported). Use copy_properties to denormalize. → match_on / match_with Operator Reference

Module-agnostic resources & composition

It does not matter which module (or the local data dir) creates a resource — in the unified graph it is the same resource (identity = type + primary key). Ship minimal reference assets for foreign types your module references; do not declare schemas for shared types (hard conflict: conflicting column specifications); declare expectations with [requirements.<type>]. One --data-dir (authoritative assets); compose logic with repeated --module; module asset files auto-bootstrap but never overwrite local files. → Composing Multiple Modules, module.toml [requirements]


2. Graph Read/Write

Mutation semantics + async rebuild

POST /api/mutations/<asset> adds (409 on existing PK); PUT /api/mutations/<asset>/<name> merges; DELETE removes. After a mutation the graph rebuilds asynchronously — never assume read-after-write consistency; poll for the expected state (typically < 2 s). In merge-updates, omit fields you must not clobber (tokens, expiry). → Asset Management API

Foreign references as plain properties

Store cheap foreign keys (e.g. order_id) as declared columns for display/filtering/action templating; use model relations for topology. Keep both in sync at write time.

Mirror workflow artifacts onto the primary resource

Action/env templating only sees origin_resource.<property>. When a workflow artifact is produced for a resource (a CSR, a report), the writer mirrors it onto the primary resource so bundles and outputs can inject it directly. Clear stale mirrors when a new cycle starts.


3. Services & Protocol Front-Ends

Protocol gateway as a long-running service action

Implement the wire protocol in a small stateless script, run it as run_mode = "service" + auto_execute = true, expose it via [runtime.expose] (Unix socket), and route with action:// in proxy.toml. All state lives in the graph — the gateway is restartable.

Record intent first, fulfill asynchronously

Accept the request and record intent (pending/processing rows) immediately; let actions or workers fulfill later. Poll endpoints render live graph state via per-resource outputs. → Outputs


4. Lifecycle & Idempotency

Explicit state machine with terminal states

pending → ready → processing → valid / invalid (+ expiry as terminal). Every failure path must land in a terminal state explicitly.

Reuse non-final requests; renew on terminal

On intake, return an existing non-final, unexpired item for the same subject (idempotent retries); create a new one only when the previous is terminal or expired (renewal semantics).

Real, computed expiry — never hardcoded dates

Compute expires = now + TTL at creation; omit expiry from merge-updates so the stored value survives; treat missing/unparseable as expired (fail-closed).

Guarded, non-regressing transitions

Transition only from the expected predecessor; reject transitions on terminal items with 409 + typed problem document; make repeated calls idempotent (re-finalizing is a no-op); merge only status fields — never overwrite identifying material (tokens, keys).

Fail-closed intake gates

Validate the subject against a registry asset before any writes; reject with the protocol’s canonical error and create zero graph rows on failure.


5. Actions & Execution

Dynamic action expansion

One template + origin_resource + match_on = one concrete action per matching node (do-thing-<name>). Expansion re-runs on every graph rebuild — actions appear/disappear with the matching state. Nodes without the discriminating property simply expand nothing. → Dynamic Actions

Air-gapped bundles: self-contained by construction

Everything the executor needs is injected at bundle-creation time: inputs (payload.json), graph data via [env] from origin_resource properties, files, and sealed secrets. POST /api/actions/... queues; ?download=true returns the bundle. → Action Triggers

Secrets via [vault_secrets]

Declare collection+secret; the controller seals to the runner’s X25519 key. Without a vault, placeholders render literally and execution fails — document seeding; fail visibly. → Action Security

Script write-back with offline fallback

Scripts always produce a local artifact and print it; if the controller URL is reachable they also write back via PUT-merge (result → active, tracker → valid; failure → invalid, non-zero exit); offline they print exact curl commands for the operator.


6. Outputs & Delivery

Address artifacts by primary key

Key outputs by origin_resource.name and use that name as the public identifier everywhere — synthetic ids that don’t match the output key produce permanent 404s.

Scalar-only interpolation

{{ origin_resource.<field> }} renders scalars; relation fields render as [[object]]. Traverse relations explicitly ([0].name, {% for %}). → Output Tera Templates


7. Dashboard / UI

Resilient refresh (cache-last-good)

Keep the last successfully loaded dataset; on refresh failure keep tables rendered and show a toast + staleness indicator; only a first-load failure shows an empty-state error row. Degrade per-entity: fall back to base fields when a relation field is absent from the schema.

Lazy-fetch heavy fields

Exclude large blobs (PEM/CSR) from periodic list queries; fetch them on demand in the click path.


8. Governance

Compliance-as-code: violations as graph resources

Controls materialize findings as policy_violation resources linked to the offender, named deterministically (violation-<rule>-<resource>); dashboards group without bespoke logic. → Policies

Contracts for validated writes

Curated, typed mutations ([[input]] with allowed_values, templated [[write]]) are the public API; raw asset mutations are the internal one. → Policies / Contracts


9. Testing & Operations

Sandbox testing

Copy the data dir, serve on an alternate port, point gateways/workers at the sandbox; inject failures via PUT (invalid, expired timestamps). Never test against live data.

Seed data as executable documentation

Seed rows cover happy path and known violations so compliance and UI states render on first boot. Dedupe polluted runtime state via the mutation DELETE API (server syncs deletions to CSV).

Registry/reference data is allowed to be “orphaned”

Unused registry rows (e.g. algorithm catalogs) intentionally have no relations. Orphan warnings are expected — never combine such modules with --remove-orphaned-assets.


10. Top Anti-Patterns

Anti-Pattern Do Instead
Hardcoded dates/URLs in writes Compute at creation; omit from merge-updates
Read-after-write without polling Treat rebuild as async; poll for expected state
Synthetic artifact ids Address artifacts by primary key
Regressing/clobbering transitions Guarded transitions; merge status fields only
Create-on-every-call intake Reuse non-final; renew on terminal/expired
Backend decisions in client config Registry-driven, graph-resolved routing
Bundles that “call home” for data Inject graph data via env at render time
Plaintext secrets in repo [vault_secrets] + sealed dispatch
Blank screen on refresh failure Cache-last-good + staleness indicator
Failures leaving items processing Always land in a terminal state
Schemas for shared types in 2 modules Undeclared shared CSVs + [requirements]
Composing modules with repeated -d One -d for assets; compose with -m