Anvil is a declarative, multi-organization AWS execution engine for running consistent, repeatable tasks across large numbers of AWS accounts, with explicit guarantees around ordering, isolation, and observability.
It provides a structured way to define what should run (tasks and dependencies) and where it should run (organizations and accounts), while the engine manages authentication, role assumption, concurrency, failure handling, and result aggregation.
Anvil is intentionally task-agnostic. Tasks are implemented as simple Python modules with a well-defined contract, enabling teams to build inventory, validation, enforcement, or reporting workflows without coupling business logic to the execution engine. Account execution is parallelized per organization using bounded worker pools, with optional fail-fast semantics.
- When using the uv tool, there are several ways to run and install dependencies. Here are a few examples:
- Manual setup (similar to pip-tools):
- Create a Python virtual environment: uv venv or python -m venv .venv
- Activate the virtual environment: ..venv\Scripts\activate.ps1
- Install dependencies: uv pip install --requirements pyproject.toml
- Manual setup (similar to pip-tools):
- uv sync:
- Sync the project's dependencies with the environment: uv sync
- Activate the virtual environment: .venv\Scripts\activate
- uv run:
- Run a command in the project environment.: uv run example.py
- Note that if you use uv run in a project, i.e. a directory with a pyproject.toml, it will install the current project before running the script.
We have multiple global commands
anvil auth …
anvil graph …
anvil tasks …
anvil run …Authentication checks validate AWS credentials and access without executing any tasks.
anvil auth check --helpAuthenticate credentials from an organization file.
anvil auth check --org-file ./yaml/orgs.yamlOutput authentication results as JSON
anvil auth check --org-file orgs.yaml --jsonSuppress all output and rely on the exit code only (useful for CI)
anvil auth check --org-file orgs.yaml --quietDisplay the resolved task dependency graph for an organization configuration.
anvil graph --helpGenerate a dependency graph from an organization file.
anvil graph --org-file .\examples\07-optional-task-semantics.yaml
Execution Graph (optional-semantics-org)
----------------------------------------
inventory
└── reporting
└── cleanupOutput graph results as JSON
anvil graph --org-file .\examples\07-optional-task-semantics.yaml --json
{
"organization": "optional-semantics-org",
"tasks": [
{
"name": "inventory",
"depends_on": []
},
{
"name": "reporting",
"depends_on": [
"inventory"
]
},
{
"name": "cleanup",
"depends_on": [
"reporting"
]
}
]
}List all available stock and user-defined tasks
anvil tasks list
Available tasks:
- inventory [plugin: my-project]
- noop [stock]
- remove_iam_user [stock]
- cleanup [plugin: my-project]
...Validate all available stock and user-defined tasks:
anvil tasks validate
[ERROR] task validation failed:
- task 'cleanup' is missing required run() parameters: ['account_alias']
- task 'inventory' is missing required run() parameters: ['metadata']anvil run --helpExecute all configured organizations and accounts
anvil run --org-file ./yaml/orgs.yamlYou can run --include, --exclude, or --dry-run to overide the yaml file if you want to just test something or run on certain accounts
# Include only specific accounts:
anvil run --org-file orgs.yaml --include 111111111111 222222222222
# Exclude specific accounts:
anvil run --org-file orgs.yaml --exclude 333333333333 444444444444Anvil supports project-local tasks in addition to its stock tasks. This allows you to add custom behavior without forking Anvil.
Tasks are resolved in the following order:
Anvil discovers tasks from two sources:
-
Stock tasks - tasks shipped with Anvil (anvil.tasks)
-
Plugin tasks - tasks registered via the anvil.tasks entry-point group
Directories named tasks/ are conventional only and are not automatically scanned.
Create a directory anywhere in your project:
my-project/
├─ tasks/
│ ├─ inventory.py
│ ├─ cleanup.py
│ └─ tagging.py
Each task module must define a callable run() function.
[project.entry-points."anvil.tasks"]
project = "tasks"Note: Your project might need to be installed pip install --editable .
You should see some path output via uv run python -c "import anvil; print(anvil.__file__)"
Each task module must define a callable run function.
This is the minimum interface required for Anvil to discover and execute a task.
def run(
*,
account_id: str,
account_alias: str,
session,
dry_run: bool,
metadata: dict,
):
"""
Execute the task for a single AWS account.
"""account_id- AWS account ID currently being processed.account_alias- Friendly name of the account.session- A boto3 Session already scoped to the target account.dry_run- Indicates whether the task should make changes.metadata- Organization metadata defined in the configuration file.
The return value is optional. Any returned data may be included in execution results.
While only the run() function is required, tasks can optionally use Anvil-provided utilities to produce structured results or record actions.
For example, tasks may import helpers such as:
from anvil.task_definition import ActionRecorderThis helper allow tasks to:
- record planned or executed actions
- produce structured output for reporting
- integrate with Anvil’s execution summaries
You can view examples of this here ActionRecorder
Using these utilities is not required, but recommended for tasks that modify infrastructure or need richer audit output.
Once configured, custom tasks behave exactly like stock tasks:
tasks:
- name: inventory
- name: cleanup
depends_on: [inventory]