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
[[link_resources]] for non-convention links
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 |