Task Contract
Anvil keeps provider business logic in plain Python task modules while the engine handles provider authentication, target resolution, dependency ordering, bounded concurrency, and result aggregation.
Task Packages and Discovery
Task compatibility is determined by the package that contributes the module:
anvil.providers.tasks.<task>is universal.anvil.providers.aws.tasks.<task>is AWS-only.anvil.providers.azure.tasks.<task>is Azure-only.anvil.providers.gcp.tasks.<task>is GCP-only.anvil.providers.github.tasks.<task>is GitHub-only.
Third-party packages register task package roots through entry points:
[project.entry-points."anvil.providers.tasks"]
universal-tasks = "company_anvil.tasks"
[project.entry-points."anvil.providers.aws.tasks"]
aws-tasks = "company_anvil.aws_tasks"
[project.entry-points."anvil.processors"]
processors = "company_anvil.processors"
[project.entry-points."anvil.provider_packages"]
providers = "company_anvil.providers"
Each public Python filename is the task name. Discovery records component names and sources without importing every implementation; Anvil imports only selected tasks during normal execution. Duplicate applicable task names are rejected as ambiguous and report every conflicting source.
For a provider collection, each immediate child package is a provider and must
expose create_provider_instance(). Processor package roots use the same
public-module discovery pattern as task packages.
Use these commands to inspect and validate discovery:
anvil list --tasks
anvil list --tasks count_vpc --detail
anvil validate --tasks
anvil validate --tasks count_vpc noop
Runtime Contract
Every task module defines a callable run(...) function. The provider-neutral
runtime keyword arguments are:
from anvil.actions import ActionRecorder
def run(
*,
provider: str,
execution_target_id: str,
execution_target_name: str,
execution_target_type: str,
region: str,
session,
dry_run: bool,
metadata: dict[str, object],
actions: ActionRecorder,
) -> dict[str, object] | None:
"""Run the task for one provider execution target and location."""
provider: selected provider name.execution_target_id: provider-specific ID, such as an account, subscription, project, owner, or repository ID.execution_target_name: provider-supplied display name.execution_target_type: provider-specific type such asaccount,subscription,project,organization, orrepository.region: current provider region or location; GitHub usesglobal.session: provider session scoped to the current target and location.dry_run: whether task mutations should be suppressed.metadata: target metadata from the YAML configuration.actions: recorder for planned or completed audit actions.
Anvil invokes tasks with keyword arguments. A task may accept **kwargs, but
explicit parameters make the contract and generated detail output clearer.
Positional-only parameters are unsupported.
Task code can also declare a task_context parameter to receive the complete
immutable TaskCallContext; the nine arguments above remain the required
public contract unless **kwargs is accepted.
Session Objects
The session interface is provider-specific:
- AWS tasks receive a boto3-compatible session with lazy client caching for the current account and region.
- Azure tasks receive an Azure session containing the credential, subscription ID, and location.
- GCP tasks receive a GCP session containing credentials, project ID, quota project, and region.
- GitHub tasks receive a GitHub session/client context for the current owner or repository.
Universal tasks should avoid assuming a provider SDK unless they branch on
provider. Provider-specific tasks should validate the execution target type
they require and provide actionable dependency errors.
Task Scope
Tasks are region-scoped by default. A module can opt into target scope:
TASK_SCOPE = "target"
A region-scoped task runs once per resolved region or location. A target-scoped task runs once per execution target using its first resolved location. A task can run only when the selected provider advertises support for that scope; AWS currently supports region scope, while Azure, GCP, and GitHub support both region and target scope.
Detail Documentation
Every task needs detail documentation. Add a Google-style docstring to
run(...); a module docstring is accepted as a fallback. This text powers
anvil list --tasks <name> --detail and is checked by task validation.
Document the operation, provider and scope assumptions, metadata keys, return shape, and important exceptions. Never include credentials or secret values in task detail text or results.
Returned Results
A task may return any JSON-serializable value. Anvil stores it in the task
result's result field and includes it in flattened JSONL output.
def run(
*,
provider: str,
execution_target_id: str,
execution_target_name: str,
execution_target_type: str,
region: str,
session,
dry_run: bool,
metadata: dict[str, object],
actions,
) -> dict[str, object]:
client = session.client("ec2")
vpc_ids = [
vpc["VpcId"]
for page in client.get_paginator("describe_vpcs").paginate()
for vpc in page.get("Vpcs", [])
]
return {
"account_id": execution_target_id,
"region": region,
"vpc_count": len(vpc_ids),
"vpc_ids": vpc_ids,
}
Returned results work best for inventory, counts, findings, identifiers, measurements, and other structured task data.
ActionRecorder
Use ActionRecorder for a concise audit trail of planned or completed work:
if dry_run:
actions.record("(dry-run) Would update the repository setting")
else:
actions.record("Updated the repository setting")
Tasks may use returned data and recorded actions together. Dry-run-aware mutation tasks should record the planned operation without issuing the provider mutation.
Validation
anvil validate --tasks performs structural validation without running tasks
or calling provider APIs. It verifies:
- task names are non-empty and unambiguous
- modules expose a callable
run(...) - the runtime signature accepts all required keyword arguments
- positional-only parameters are absent
- detail documentation is present
Configuration validation additionally checks that each configured task is compatible with the selected provider and task scope.
Dependency-Aware Execution
Tasks execute in dependency order within each execution-target/location stream. If a dependency fails, dependent tasks are recorded as blocked. Optional task failures are recorded without necessarily failing the execution target; non-optional failures stop further work for that stream and participate in fail-fast cancellation.