Destiny
Destiny is a declarative description of a single host’s desired state: a set of idempotent steps of the form core.<module>.<state>. It is the atomic building block of Soul Stack — “how to bring a single host to the desired state,” without orchestration (which hosts exactly, and in what order — that’s already Scenario).
Destiny is isolated: it sees only its own declared input (input:) and its own host’s facts. It knows nothing about the database or the scenario that called it, and it never peeks at anyone else’s context.
Layout
Section titled “Layout”A Destiny is a folder in a git repository:
destiny-<name>/├── destiny.yml # manifest: input/output contracts├── tasks/│ ├── main.yml # entry point: the task list│ └── <sub>.yml # includable neighbors (include)├── templates/ # file templates (.tmpl)└── vars.yml # the destiny author's local valuestasks/main.yml is a task list, executed top to bottom. There is no top-level wrapper: the file path itself supplies the context.
A minimal task addresses one module in the form core.<module>.<state> and passes parameters:
- name: Install redis-server package module: core.pkg.installed params: name: redis-serverThe module name has three levels: core.pkg.installed reads as “package installed” — a desired state, not an “install it” command. Available modules and their states are in the core module catalog.
Idempotency
Section titled “Idempotency”Every step is idempotent: the module first checks the host’s current state and changes it only if it differs from the desired one. The step’s result is changed=true (the state changed) or changed=false (it was already that way). On a repeat run, a state that has already been reached changes nothing.
The changed status is a signal for dependencies between steps (for example, “restart the service only if the config changed”).
Task blocks
Section titled “Task blocks”Beyond module: and params:, a task supports control blocks. The most common ones:
| Block | Purpose |
|---|---|
name: | A human-readable step name (goes into the apply log). |
when: | A condition (CEL): the task runs only if the expression is true. |
register: | The name under which the step’s result is saved for later tasks. |
vars: | Local values available inside the task. |
loop: | Repeat the step over the elements of a collection. |
changed_when: | Overrides whether the step counts as having changed state. |
failed_when: | Overrides whether the step counts as failed. |
retry: / timeout: | Retries and a time limit per attempt. |
no_log: | Hide the step’s parameters/result from logs (for secrets). |
Dependencies between steps (requisites)
Section titled “Dependencies between steps (requisites)”Dependencies are expressed through the register: names of other tasks:
| Block | Semantics |
|---|---|
require: | Don’t start until the named tasks have finished (a barrier). |
onchanges: | Run only if the named task changed state. |
onfail: | Run only if the named task failed (rescue/cleanup). |
The classic example — restarting a service when the config changes:
- name: Render redis.conf module: core.file.present register: redis_conf params: path: /etc/redis/redis.conf content: "..."
- name: Restart redis-server because config changed module: core.service.restarted onchanges: [redis_conf] params: name: redis-serverThe restart happens only if the config render reported changed=true.
Grouping and inclusion
Section titled “Grouping and inclusion”block:— an inline group of several tasks sharing onewhen:condition: if it’s false, the whole group is skipped at once, without repeating the condition on each task.include:— pulls in a neighboring file from the sametasks/folder: its tasks are spliced in at the include task’s position. For reusable or large groups.
Rendering files from templates
Section titled “Rendering files from templates”A file assembled from a template is rendered by the core.file.rendered step. Values for the template are explicitly lifted into the step’s vars: — the only channel for getting data into the file:
- name: Render redis.conf from template module: core.file.rendered params: path: /etc/redis/redis.conf template: templates/redis.conf.tmpl vars: maxmemory: "${ essence.redis.maxmemory }" mode: "0640"Inside templates/redis.conf.tmpl the lifted value is available as {{ .vars.maxmemory }}. The template sees only what the author explicitly lifted, plus a narrow set of host facts — not the whole context. More on the engine boundary in the templating overview.
Input and output contracts
Section titled “Input and output contracts”Destiny declares two optional contracts in destiny.yml, symmetric in form:
input:— what Destiny accepts from the outside. Whoever calls Destiny must pass values per this contract; they are validated (types, required-ness,pattern/enum) before apply. In tasks the input is available asinput.<name>.output:— what Destiny publishes outward as its result. The top-leveloutput:declares the result schema, and tasks fill the declared fields through their own task-leveloutput:. Destiny publishes its result but never reads anyone else’s context — isolation stays intact.
input: version: type: string required: true maxmemory: type: stringoutput: config_path: type: stringIf Destiny returns nothing, the output: block is omitted — the caller still gets the standard changed / failed.
Template context
Section titled “Template context”In Destiny expressions you can reach:
| Name | Contents |
|---|---|
input.<name> | Values of the declared input contract. |
vars.<name> | The Destiny author’s local values (vars.yml + task-level). |
soulprint.self.<path> | The host’s own facts: soulprint.self.os.family, soulprint.self.network.primary_ip, … |
register.<name>.* | Results of previous steps: .changed, .failed, and fields from their output:. |
What is not in the Destiny context (that’s the scenario level): direct access to essence.*, to the caller’s attributes, and cross-host queries against other hosts’ facts. Destiny receives these values only through its own input:.
Full example
Section titled “Full example”- name: Install redis-server package module: core.pkg.installed retry: { count: 3, delay: 10s } # the network may flicker params: name: redis-server version: "${ input.version }"
- name: Render redis.conf from template module: core.file.rendered register: redis_conf params: path: /etc/redis/redis.conf template: templates/redis.conf.tmpl vars: maxmemory: "${ input.maxmemory }" mode: "0640"
- name: Ensure redis-server is running and enabled at boot module: core.service.running params: name: redis-server enabled: true
- name: Restart redis-server because config changed module: core.service.restarted onchanges: [redis_conf] timeout: 30s params: name: redis-serverWhat’s next
Section titled “What’s next”- Scenario — orchestrating Destiny across a cluster.
- Essence — where parameter values come from.
- Core module catalog — what the built-in modules can do.