Skip to content

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.

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 values

tasks/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-server

The 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.

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”).

Beyond module: and params:, a task supports control blocks. The most common ones:

BlockPurpose
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 are expressed through the register: names of other tasks:

BlockSemantics
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-server

The restart happens only if the config render reported changed=true.

  • block: — an inline group of several tasks sharing one when: 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 same tasks/ folder: its tasks are spliced in at the include task’s position. For reusable or large groups.

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.

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 as input.<name>.
  • output: — what Destiny publishes outward as its result. The top-level output: declares the result schema, and tasks fill the declared fields through their own task-level output:. Destiny publishes its result but never reads anyone else’s context — isolation stays intact.
destiny.yml
input:
version:
type: string
required: true
maxmemory:
type: string
output:
config_path:
type: string

If Destiny returns nothing, the output: block is omitted — the caller still gets the standard changed / failed.

In Destiny expressions you can reach:

NameContents
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:.

tasks/main.yml
- 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-server