Engines

daedal: the composition engine

Version of 2026-09-22, daedal@8b84d7a · Markdown · Worked example

Lachlan Douglas

An engineer declares a system as components and the relationships between them, and daedal resolves that declaration to one determinate form before anything runs. The same declaration gives the same form on any machine. A plan states what a change will do before anyone applies it. daedal hands the building and running to the substrates a team already uses.

1 What daedal is

daedal is a composition engine: a single statically linked Rust binary that runs in process, with no daemon. An engineer runs it from a solution directory. daedal compiles the declared composition to a normal form, plans the change against what it last applied and applies the change by handing each step to a substrate. daedal keeps a solution's state in the solution directory.

Terraform manages infrastructure state, Kubernetes manages cluster state and Restate runs durable workflows. Each is a control plane in its own domain. daedal works at the level of the solution: which components make it up, how they connect and what each expects of the others. daedal composes the solution's components and delegates each one to the tool that realises it. A composition can declare a database cluster and the release that uses the cluster. One apply realises both, in the order their relationships imply.

A substrate is anything that realises part of the resolved form: a cloud provider, a container runtime, a scheduler, a durable-execution engine, an inference agent or a person working through an interface. daedal addresses each substrate by the role it fills in the composition. Two substrates that fill the same role are interchangeable. An engineer swaps one for the other by changing a binding, and every consumer of the role keeps its declaration.

No Feedback gives the account of a made thing whose form is settled by its declared parts before it operates. Free Assembly gives the calculus of the composition operator that daedal's resolver implements. Free Assembly also proves that the resolved form is the same in any order of assembly. Both papers are at /foundations/.

2 The composition model

A solution is a tree of components under one root, each declared in a short manifest, daedal.component.yaml. Every component takes one of seven shapes:

Shape Role
root the solution's scope root; owns its children and carries the top-level wiring and grants
branch an interior container that owns a scope
module a sub-root that daedal compiles as its own unit, connected to the enclosing composition only through a declared interface
content a static leaf that emits data at resolve time and runs no code
step a component whose code runs during an apply and hands any lasting work to a substrate
scaffold a background helper opened before its first consumer and torn down after its last, even when an apply fails
aspect a cross-cutting emitter whose offers fan out to every step that accepts them

Components relate through typed relationships, which daedal resolves by reading the declarations:

Relationship Meaning
configuration static key-values sent to a component; each key has one configurer, and a second configurer of the same key is a compile error
contribution a structured fragment attributed to its emitter and collected by kind; daedal aggregates it at build time or pushes it to a substrate at run time
provision a provider supplying a consumer; daedal resolves it directly when static and routes it from a step's output when dynamic
offer an aspect's fragment, fanned out to every step that accepts it; an accepted offer with no binding is a compile error
register the edge that anchors a step to the substrate that owns it
bind a scope owner's choice of which component fills a role, for registrants and aspects
grant the operator's permissions for a component (section 6)

3 Resolution and change

daedal resolves a composition by assembling its declared parts and deriving their wiring across the whole composition at once. The result is a single normal-form graph. daedal wires a requirement to its provider when there is one, and reports an error when there are two.

Planning. daedal compares each step's freshly resolved configuration with its last applied baseline and classifies the change as create, no-op, update or replace. daedal plan also runs each step's own plan phase, so a step can report what it would change in the world.

Immutable keys. A step can mark configuration keys immutable. Changing one forces a replace.

Guarded destruction. When a replace would run a step's recorded obliterators, daedal refuses the apply until the operator authorises the replace with an obliterate flag. daedal also refuses an apply whose obliterate flag authorises nothing.

4 Execution

Scheduling. daedal runs the steps of a resolved graph over their dependency order, in parallel up to a bound that defaults to the machine's core count.

Converge always. daedal runs every step on every apply. Each step checks whether the world already matches its input. daedal tears down a component marked for removal.

Failure. By default daedal lets the running steps finish, then halts. In persevere mode it continues the independent branches and stops only what depends on the failure. After a partial apply, daedal records new state for the steps it processed and keeps the prior state of the rest. A later teardown can therefore remove everything that earlier applies created. The next apply runs every step again, and each step converges.

Suspension. A step can return a suspended result. daedal records the step as suspended, pauses that branch and ends the run cleanly. The next apply runs the step again.

Locking. Mutating operations on a solution's state take an on-disk lock, and daedal reclaims the lock when its holder has died.

5 Substrates

daedal separates two choices for each step: the language its code is written in and the place the step runs.

The step protocol

A step exchanges JSON with daedal once, statelessly, over standard streams. daedal sends the assembled input and a phase, plan or apply. The step does its work and returns a structured result, streaming log lines and heartbeats as it goes. An operator can set an optional timeout on silence. daedal then stops a step that sends no log line or heartbeat within that time, and a long-running step that keeps reporting progress runs to completion.

Language runners

A step's file extension selects its launcher. Shell steps run directly and speak the protocol themselves. Python and Go steps have SDKs. daedal builds a Go step to a static binary before running it. Elixir steps run through an escript runner. Any program that speaks the protocol can serve as a step.

Executors

The operator chooses where each step runs:

  • host, the default, runs the step on the operator's machine with the operator's privileges;
  • container runs the step in podman with no network, a read-only root filesystem, its code mounted read-only and one control channel as its route to the host. Shell, Python and Go steps run under the container executor; Elixir steps run on the host.

6 Governance

The composition declares every permission a system holds, so the operator can read them all before anything runs.

Grants

A step declares the external programs it requires. The operator grants each component its permissions, under four keys: commands, secrets, paths and executor. A path grant names an entry from the root's import: paths:, the one place an engineer declares a host directory. The operator confers grants at a scope. daedal refuses a grant made by an imported component, so an imported package holds only the authority its importer gives it. A module can grant its components at most the authority the module holds.

The control channel

A step under the container executor reaches the host only through the control channel. The step asks daedal to run a program, and daedal runs that program only when the operator has granted it to that step. The container executor makes the grants a perimeter. A step under the host executor runs directly, as an ordinary process with the operator's privileges, under grants the composition declares and the run log records. daedal injects only the step's granted secrets and gates the commands the step sends through the control channel.

Secrets

daedal resolves a secret in memory from the first of three sources that holds it: an operator mapping to an environment variable or file, the encrypted secrets file or an environment variable of the same name. daedal places the value in the environment of a granted program. A step under the container executor names a secret in a control-channel request, and daedal places the value in the granted program's environment, outside the step. A step under the host executor receives its granted secrets in its own environment.

Pre-flight

Before every plan, daedal collects every external program the composition requires, checks that each is present and reports every missing one in a single message.

7 Certification

daedal certify issues a composition certificate as JSON: a digest of the normal form, which is independent of the host, a digest of the resolved form, with the host's variables and paths bound, the engine version and a content digest for every input. daedal verify recomputes the composition and checks it against a certificate. Verification passes when the normal form and every input digest match. On a failure, daedal verify names each input whose digest changed.

8 Packaging and distribution

The package loop. pack marks a component as a package in a bundle, export projects the package into the local bundle store and import places a package into a scope by name, with its whole subtree, references, child packages and code. On import daedal mints a new identity and derives the wiring in the new context. update refreshes imported components from their recorded source and keeps their identity.

Deterministic artefacts. bundle builds a bundle as a standard OCI image layout with sorted entries and zeroed timestamps and ownership, so the same content gives the same digest on any machine.

Transports. download fetches a bundle from a local file, a git repository or an OCI registry. The git and OCI transports come with the transport build feature, which a build enables explicitly. install downloads a bundle and imports a package in one step. For private registries daedal uses the operator's Docker credentials and credential helpers. Pushing a bundle is the operator's step, with the operator's own tooling.

9 State and teardown

State. A small embedded SQLite database records each applied step's path, status and apply sequence. daedal stores each step's recorded output, including the teardown layers the step registered, in that step's output.json, next to the database. The database and the recorded outputs together describe the system as last applied.

History. daedal writes an append-only log for every run, indexed by digest. daedal logs replays a run. daedal compact deletes runs outside the operator's retention setting.

Teardown. daedal apply --down runs the destroyers each step recorded, in reverse order. Obliterators perform irreversible cleanup such as deleting data, and daedal runs them only when the operator passes an obliterate flag. daedal remove marks a component for removal, and the next apply tears the component down. Until that apply, daedal restore clears the mark.

10 Commands

Group Commands
Compose and inspect compile, show, steps
Plan and apply plan, apply (with --down for teardown)
Query state, logs, compact
Certify certify, verify
Author init, copy, move, rename, remove, restore, secrets
Package pack, export, import, update, bundle, download, install

A global -C flag runs any command against another directory.

11 Authoring tools

daedal-studio draws a solution as a live graph, redrawing as the manifests change, with a panel that shows any component's relationships. daedal-plugin is a Claude Code plugin that teaches an assistant to author daedal compositions: the shapes, the relationships and the conventions.

12 Neighbours

Much of daedal's design has prior art. Nix resolves a graph of immutable declarations before it builds. Architecture description languages compose systems from components and connectors. Workflow engines such as Temporal compose human and machine tasks. Separation logic supplies the algebraic structure Free Assembly builds on. daedal composes infrastructure, application code, durable workflow, human work and inference agents in one composition. daedal addresses each substrate by role and fixes every grant when the composition resolves.