Task Workflows and Result Sharing
Anvil tasks can form a dependency graph inside each configured target. A task
can wait for earlier tasks and select structured values from their
TaskResult objects without using global state or temporary files.
Invocation IDs
name selects a discovered component. id identifies one configured use of
that component and defaults to name when omitted:
tasks:
- name: inventory_users
- id: remove_departed_users
name: remove_iam_user
If the same component appears more than once, every occurrence needs an
explicit, unique id. Dependencies always reference effective IDs, not a
component-name fallback.
Ordering with depends_on
Declare direct dependencies with depends_on:
tasks:
- name: inventory_users
- id: remove_departed_users
name: remove_iam_user
depends_on:
- inventory_users
A normal dependent task runs only after every dependency succeeds. If a dependency errors, is interrupted, or is skipped, the dependent task is recorded as blocked.
Static Metadata vs Runtime Dependency Data
Use metadata for operator-configured values known before the run. Target
metadata is recursively merged with task metadata, and task values take
precedence.
Use dependency_data for values produced during the run:
tasks:
- name: inventory_users
- id: remove_departed_users
name: remove_iam_user
depends_on:
- inventory_users
metadata:
reason: employment-ended
dependency_data:
users:
task_id: inventory_users
path: result.departed_user_ids
The consuming task receives the selected values in its keyword-only
dependency_data argument:
def run(*, metadata: dict[str, object], dependency_data: dict[str, object], **kwargs):
reason = metadata["reason"]
users = dependency_data["users"]
return {"reason": reason, "selected_count": len(users)}
Treat metadata and dependency_data as read-only. Anvil copies their nested
mappings and lists for each invocation.
Dependency Paths
Each dependency_data entry names a direct dependency and can select:
| Configuration | Value passed to the consumer |
|---|---|
Omit path |
Complete producer TaskResult |
path: result |
Producer return value |
path: result.users |
Nested mapping value |
path: status |
Producer status |
path: error |
Producer error value |
path: actions |
Producer recorded actions |
Paths use dotted mapping fields. List indexing is not supported. Existing null
values are valid; a missing path causes the consumer to fail before its
run() function is called.
Cleanup with always_run
Set always_run: true on a task with at least one dependency when cleanup or
restoration must run after dependencies settle, including unsuccessful ones:
tasks:
- id: detach_guardrails
name: reconcile_config_guardrails
metadata:
attachment_state: absent
- id: restore_guardrails
name: reconcile_config_guardrails
depends_on:
- detach_guardrails
always_run: true
metadata:
attachment_state: present
dependency_data:
attachments:
task_id: detach_guardrails
path: result.attachments
always_run changes eligibility; it does not erase an upstream failure.
Preserve Recovery Data on Failure
A task that partially mutates resources can raise TaskExecutionError with a
JSON-serializable partial result. Downstream cleanup can select that recovery
data:
from anvil.task_errors import TaskExecutionError
raise TaskExecutionError(
"Mutation partially failed",
partial_result={"attachments": detached_attachments},
)
Pair this with an always_run cleanup task that selects
result.attachments.
Scope-Aware Fan-Out and Fan-In
Task scope is declared in Python with TASK_SCOPE; it is not configured in
YAML. Region-scoped tasks run for every resolved location. Target-scoped tasks
run once per execution target. Providers that support configured_target
tasks can also run once for the complete YAML target.
When scopes differ, Anvil maps dependency results across the execution graph:
- a configured-target producer can fan out to target or region consumers;
- region results can fan in to a configured-target consumer;
- ordering remains deterministic even when regions run concurrently.
The selected value is a single object when one producer invocation maps to the
consumer. It is a list when multiple producer invocations fan in. For example,
a configured-target consumer selecting path: result from a region task
receives one return value per mapped region in deterministic plan order. Write
consumers to validate both the expected container shape and the selected value
types.
See the complete
configured-target cleanup workflow
for fan-out, fan-in, recovery data, and run_on_failure reporting.
Workflow Checklist
- Give repeated task components explicit IDs.
- Depend only on tasks whose completion is required.
- Select only the result fields a consumer needs.
- Keep static operator intent in
metadataand runtime values independency_data. - Make every returned value JSON-serializable.
- Use
always_runonly for genuine finalization or recovery. - Validate the graph with
anvil validate --config-file workflow.yaml.