DSL
Soul Stack is described by three kinds of YAML artifacts. They are all typed; expressions inside them are CEL, and file rendering is Go text/template.
| Artifact | What it describes | Where it lives |
|---|---|---|
| Destiny | The desired state of a single host — a set of idempotent steps. | git (the destiny repository) |
| Scenario | Destiny plus orchestration: which hosts, in what order, under what conditions. | git (the service repository) |
| Essence | Parameters and secrets bound to a host or group. | git (defaults) + database (overrides in spec) |
How the three artifacts connect
Section titled “How the three artifacts connect”- Destiny — the atomic building block: “how to bring a single host to the desired state.” Isolated — it sees only its own declared input.
- Scenario — an operation over a whole cluster: “how to run
create/add_user/restartacross a set of hosts.” A scenario receives the full set of Destiny task blocks plus the orchestration delta (which hosts, in what order) and can call Destiny throughapply:. - Essence — the values fed in at render time: application settings, passwords, keys. A scenario sees Essence directly; Destiny receives values only through an explicit hand-off on its input.
The boundary between Destiny and Scenario is a recommendation, not a hard wall: anything reusable, critical, or worth isolating goes into a separate Destiny; one-off logic for a specific operation is fine inline in the scenario.
Templating model
Section titled “Templating model”Soul Stack has two expression engines, and the boundary between them runs strictly per file:
| Context | Engine | Marker |
|---|---|---|
Expressions in YAML: conditions (when:, where:, …) | CEL | the whole string = an expression, no wrapper |
| Value interpolation inside YAML strings | CEL | ${ … } |
Template files with the .tmpl extension | Go text/template | {{ … }} |
Only one engine runs per file: CEL never executes inside a .tmpl, and text/template never executes inside a .yml. The two don’t overlap — they hand data off in sequence: CEL computes values in the YAML, and then the explicitly lifted values flow into the file render.
CEL — expressions in YAML
Section titled “CEL — expressions in YAML”CEL handles every expression in YAML artifacts. Two positions:
-
Conditional keys (
when:,where:,changed_when:,failed_when:,until:) — the whole string is treated as a CEL expression, with no wrapper:when: input.do_restartwhere: register.role.stdout == "master" -
Interpolation in string values (
params:,vars:, input hand-off) — the expression is wrapped in${ … }:params:command: "redis-cli replicaof ${ register.master.stdout } 6379"replicas: "${ input.replicas * 2 }"
CEL is a side-effect-free expression engine: it has no access to the filesystem, to commands, or to arbitrary network. Expressions can reach the host’s facts (soulprint.self.<path>), parameters (input.*, essence.*), results of previous steps (register.*), and a set of built-in functions (checks, size, regexes, time handling).
Go text/template — file rendering
Section titled “Go text/template — file rendering”Template files with the .tmpl extension are rendered by the Go text/template engine. This is the only place it runs, and it is used only by the dedicated step that renders a file on the host.
Rendering is safe by default: strict mode (referencing a missing field is an error, not an empty string), a closed allow-list of functions (no running commands, reading the environment, or generating randomness), and an isolated context (the template sees only the explicitly lifted values, not the whole scenario context).
Secrets
Section titled “Secrets”Secrets are resolved through Vault on the server side at render time. The expression engine treats them as ordinary values — otherwise you could not legitimately pass a secret into a parameter. Masking is applied on output: secrets never appear in cleartext in logs, traces, API responses, or run reports.
Static checking
Section titled “Static checking”Artifacts can be checked before apply with the offline linter soul-lint: it parses the YAML, verifies module and state names, and checks parameter schemas and expressions — on a workstation or in CI, without executing anything on hosts.