Declaring the graph
Concepts
Declaring the graph
Section titled “Declaring the graph”The declaration language has exactly three item kinds: node, pool and
executor. Each item is one logical line of comma-separated clauses, ending
in ;. A useful graph is often just names, modes, deps and workers:
supervisor_graph! { node NET = Terminate, task: net_task; node HTTP = Terminate, deps: [NET], task: http_worker;}Everything else is an optional clause on those lines. The full shape:
supervisor_graph! { name: IDENT; // rename the emitted GRAPH static executor NAME; // a runtime-filled spawner slot default executor NAME; // ..inherited by every task:/spawn:-fn item
node NAME = Mode, // Terminate | Pause | OnDemand deps: [A, POOL, NET ready] // start order; `ready` also waits , task: worker // or `spawn: task_fn` , resources: [R: Type, ..] // owned values handed at spawn , provides: [R, ..] // slots this task fills , exit: Type // capture the return value , state: Type = expr // per-activation boxed state , cancel // shell owns shutdown, no node , reads: [crate::SIG, ..] // declared dataflow , writes: [crate::SIG beat] // entry markers: observed, beat, veto , discover // bind tables derived from code , dataflow: [crate::setter] // adopt an accessor's tables , beat_timeout: MS, ready_on_write , pool_size: N, executor: NAME , slot_timeout: MS, ack_timeout: MS, disabled;
pool NAME = [Mode, ..], // one mode per member, floor first deps: [..], task: worker, resources: [..], policy: DeferredShrink::new(..), min: N, max: M, slot_timeout: MS, ack_timeout: MS;}Regular reading rules:
- Position has meaning exactly twice: the optional
name:header is the first item, and the mode sits right after=(a pool takes a bracketed mode list there, one mode per member, floor first). - Everything else is keyword-dispatched and may appear in any order:
top-level
node,pool,executorandobserveitems mix freely, and node and pool clauses reorder freely (deps:is a clause like any other, and optional). - Every clause is inline on its item. There are no block forms and no
top-level
resources { }section. - Resource slot names are unique across the whole graph; only
sharedentries may repeat a name. detachedis not a mode or a clause: a task makes itself detached at runtime withnode.set_detached(true).
task: vs spawn:
Section titled “task: vs spawn:”Two ways to name the worker. Prefer task:: it names a plain async fn,
possibly generic, and the macro stamps the #[embassy_executor::task] shell
for you.
async fn sensor<D: Driver>(node: &'static TaskNode, dev: D) { /* ... */ }
supervisor_graph! { node BME = Terminate, task: sensor::<Bme280>(bme_dev()); node SHT = Terminate, task: sensor(sht_dev()); // turbofish optional}task: also admits generic workers, which embassy task fns normally reject:
one worker, one node per concrete instantiation, each monomorphized into its
own shell. Arguments in the partial call are evaluated inside the shell at
the task’s first poll, on the node’s own executor: good for building
resources where they run, wrong for values that must be snapshotted at spawn
time. An argument that might not exist yet at first poll does not belong
here; that is what resources: is for.
spawn: names a hand-written #[embassy_executor::task] fn. It is the right
tool in four cases:
- The fn already carries the attribute and you cannot strip it (another crate, other callers).
- The same task is also spawned outside the graph, so you want the one existing task pool, not a second one.
- You need a verbatim closure for custom spawn-time logic. Call
NODE.adopt(&token)inside it or the node stays invisible to tracing; nothing will remind you. - An argument must be evaluated at spawn time on the supervisor’s executor, for example a counter snapshot an interrupt-tier task would otherwise read late.
Omitting both makes the node parked: the application spawns it by hand
(typically a Pause task holding a peripheral) and the supervisor tracks it
without ever spawning it.
resources:, provides:, exit:, state:
Section titled “resources:, provides:, exit:, state:”Each is a page or a section of one:
resources:hands owned values to workers through slots, withconsume,shared,divisibleandlocalkinds.provides:names the slots a node fills at runtime; they are cleared when it stops.exit:captures a worker’s return value in a slot you can await.state:boxes per-activation heap state that is freed when the task exits.
Dependencies and markers
Section titled “Dependencies and markers”deps: names nodes or pools. A pool name resolves to its floor member, so
deps: [WORKERS] means “start once the pool’s always-on member is up”.
Markers refine what a dependency means:
ready(featurereadiness): the spawn additionally waits for the dep’s task to callset_ready().ready bound(featurebound-deps): additionally, if that provider later withdraws readiness, the dependent is stopped, and it comes back when the provider does.
The whole story, including budgets, is in Dependencies and gating.
executor slots
Section titled “executor slots”supervisor_graph! { executor HIGH; // a spawner slot, filled at runtime
node SAMPLER = Terminate, executor: HIGH, task: sampler_worker;}executor NAME; declares a spawner slot. Fill it at runtime with a SendSpawner
from an InterruptExecutor or another core’s executor. Nodes marked with
executor: NAME spawn through it. Bring-up waits for the slot, so a late-booting
core becomes a rendezvous instead of a race.
default executor NAME; sets that slot as the default executor for every node
and pool that could have used executor: NAME themselves. task: workers and
spawn: functions inherit it; parked nodes and verbatim spawn: closures stay
on the supervisor’s executor. An explicit executor: always overrides the
default. Only one default executor is allowed per graph, at the compose site, and
it cannot be #[cfg]-gated. Details in
Executors and cores.
pool WORKERS = [Terminate, OnDemand, OnDemand], deps: [NET], task: http_worker, policy: embassy_supervisor::DeferredShrink::new(Duration::from_secs(4)), min: 1, max: 3;The mode list declares the members, floor member first; it is the only
positional part. The clauses after it are order-free, like a node’s: deps:,
optional executor:, the worker, resources:, policy:, min:, max:,
optional slot_timeout:, ack_timeout: and cancel. min and max accept
const expressions, validated so that min <= max <= member count. The emitted
constants WORKERS_MIN / WORKERS_MAX / WORKERS_MEMBERS exist for
const-context sizing, for example deriving a socket budget from the worker
budget. See Elastic pools.
Feature gates
Section titled “Feature gates”Constructs behind Cargo features (ready, bound, observed, veto,
divisible, serialized, local, state:, beat_timeout:) always
parse; whether your build permits them is policy applied afterwards.
Using one without its feature is a compile error that names the feature.
#[cfg(...)]
Section titled “#[cfg(...)]”Allowed on any node or pool, on individual deps, and on individual resource
entries (gate the worker’s matching parameter with the same attribute). A
node compiled out keeps its slot as None and is skipped everywhere.
What the compiler checks
Section titled “What the compiler checks”Anything structurally wrong is an error with a span on the offending token:
- unknown dependency, duplicate dependency, duplicate node or pool name
- unknown
executor:name;executor:combined with a closure spawn task:andspawn:together; a closure undertask:;pool_size:withouttask:;resources:withouttask:- empty or duplicate resource names; contradictory kind markers; a
sharedslot redeclared with a different shape localwithout thelocal-resourcesfeature;localwithexecutor:divisiblemixed with another kind or given a type, or used without thebudgetfeature;divisibleon apool_size > 1entryvetoon areads:entry, without thevetofeature, on a gate with too few slots, with more than 32 writers, or with one gate spelled two waysserializedwithoutshared, or with holders spread over several executorsslot_timeout: 0orack_timeout: 0;cancelwithouttask:;cancelwithPause- pool bounds violations; a
poolwithout thepoolfeature - more than 256 slots (all graph indices are
u8) - a dependency cycle, caught by the const topological sort
The generated surface at the call site is: one pub static per node, the
pool array and its consts, one slot static per resource entry, one spawner
slot static per executor, and GRAPH. Nothing else.
Writing supervised tasks covers the other half of the contract: what your workers do with their node.