Overview
Tasks are the fundamental unit of work in Ansible. A task calls a module with arguments to achieve a desired state. Tasks appear in playbooks, task files, handlers, and blocks.
This document plans the domain types for tasks. Unlike inventory and playbook which have dedicated parser plugins, task parsing is embedded in the playbook plugin. However, tasks need their own domain schema because they're referenced by playbooks, handlers, and potentially standalone task files.
Reference: docs/ansible-schema/tasks.json
Decisions to Make
| Question | Options |
|---|---|
| Separate plugin? | No - task parsing is part of playbook plugin |
| Standalone task CLI? | Maybe - shep task validate -f tasks/main.yml |
| Module args format | Key-value pairs as [Var] (same as inventory vars) |
| Block support | Yes - blocks are task containers with rescue/always |
Domain Analysis
Task Structure (from schema)
A task can have:
Core:
name- optional description- Module invocation (one of many module names as key)
action- alternative module specificationargs- module argumentslocal_action- run on controller
Control flow:
when- conditional (string, bool, or list)loop/with_*- iterationloop_control- loop configurationregister- capture resultuntil/retries/delay- retry logicchanged_when/failed_when- result interpretation
Privilege:
become/become_user/become_method/become_flags
Delegation:
delegate_to- run on different hostdelegate_facts- assign facts to delegate target
Error handling:
ignore_errors- continue on failureignore_unreachable- continue if host unreachableany_errors_fatal- stop entire play on failure
Notification:
notify- trigger handlerslisten- handler listening (for handlers only)
Execution:
async/poll- asynchronous executionthrottle- limit parallelismtimeout- task timeoutrun_once- execute once regardless of host count
Metadata:
tags- for selective executionno_log- hide outputdiff- show change diffcheck_mode- dry-run override
Environment:
environment- environment variablesvars- task-scoped variablesmodule_defaults- default args for modules
Block Structure
A block groups tasks with error handling:
block- main task listrescue- tasks on failurealways- tasks always run
Blocks can be nested and have most task attributes (when, become, etc.).
FlatBuffers Schema Design
Domain types (flatbuffers/domain/task.fbs):
namespace shep.domain;
// Reuse Var from inventory.fbs for key-value pairs
// include "domain/inventory.fbs";
// A single task invocation
table Task {
name:string;
// Module invocation (exactly one should be set)
module:string; // module name (e.g., "ansible.builtin.apt")
args:[Var]; // module arguments as key-value pairs
action:string; // alternative: "module: args" format
// Control flow
when:string; // condition (may be complex, stored as string)
loop:string; // loop expression (stored as string/JSON)
loop_control:string; // loop control settings (JSON)
register:string; // variable name to store result
until:string; // retry condition
retries:int;
delay:int;
changed_when:string;
failed_when:string;
// Privilege escalation
become:bool;
become_user:string;
become_method:string;
become_exe:string;
become_flags:string;
// Delegation
delegate_to:string;
delegate_facts:bool;
// Error handling
ignore_errors:bool;
ignore_unreachable:bool;
any_errors_fatal:bool;
// Notification
notify:[string]; // handler names to trigger
listen:[string]; // topics to listen for (handlers only)
// Execution
async:int;
poll:int;
throttle:int;
timeout:int;
run_once:bool;
// Metadata
tags:[string];
no_log:bool;
diff:bool;
check_mode:bool;
// Environment
environment:[Var];
vars:[Var];
// Connection (overrides)
connection:string;
remote_user:string;
port:int;
}
// A block groups tasks with error handling
table Block {
name:string;
// Task lists
block:[TaskOrBlock]; // main tasks (requires union or separate messages)
rescue:[TaskOrBlock]; // on failure
always:[TaskOrBlock]; // always run
// Block-level attributes (same as task)
when:string;
become:bool;
become_user:string;
become_method:string;
tags:[string];
vars:[Var];
environment:[Var];
delegate_to:string;
ignore_errors:bool;
ignore_unreachable:bool;
any_errors_fatal:bool;
throttle:int;
timeout:int;
run_once:bool;
no_log:bool;
diff:bool;
check_mode:bool;
connection:string;
remote_user:string;
port:int;
}
// Union to represent either a task or a block
// Note: FlatBuffers unions require careful handling
union TaskOrBlockUnion { Task, Block }
table TaskOrBlock {
item:TaskOrBlockUnion;
}
// A list of tasks (used in playbooks, handlers, etc.)
table TaskList {
items:[TaskOrBlock];
}
Alternative: Simpler design without unions:
namespace shep.domain;
// Task with optional nested block
table Task {
name:string;
module:string;
args:[Var];
// ... other fields ...
// If this is a block, these are set instead of module
is_block:bool;
block_tasks:[Task]; // self-referential for blocks
rescue_tasks:[Task];
always_tasks:[Task];
}
The simpler design avoids FlatBuffers unions at the cost of a less clean schema.
Implementation Notes
Module Detection
Ansible tasks specify modules in multiple ways:
# Direct module name
- apt:
name: nginx
# With args
- name: Install nginx
apt:
name: nginx
state: present
# Action syntax
- name: Install nginx
action: apt name=nginx state=present
# Free-form
- shell: echo hello
# FQCN
- ansible.builtin.apt:
name: nginx
The parser must:
- Identify which key is the module (not a known task attribute)
- Handle FQCN and short names
- Parse both dict args and free-form string args
Conditional Complexity
The when field can be:
- Boolean:
when: false - String:
when: ansible_os_family == "Debian" - List (AND):
when: [condition1, condition2]
Store as string (JSON-encoded for lists). The host evaluates at runtime.
Loop Complexity
Loops can use:
loop:with a list or expressionwith_items:,with_dict:,with_fileglob:, etc.
Store the loop type and expression. The host expands at runtime.
File Checklist
New Files
flatbuffers/domain/task.fbs # Task, Block, TaskList
Modified Files
flatbuffers/domain/playbook.fbs # References task.fbs
Makefile # Add task.fbs compilation
Generated Files
internal/fbs/shep/domain/Task.go
internal/fbs/shep/domain/Block.go
internal/fbs/shep/domain/TaskOrBlock.go # if using unions
internal/fbs/shep/domain/TaskList.go
Milestones
- [ ] M1: Schema design finalized — Decide on union vs simple approach
- [ ] M2: FlatBuffers compile — task.fbs generates Go code
- [ ] M3: Integrated with playbook — Playbook parser creates Tasks
- [ ] M4: Block support — Blocks with rescue/always work
- [ ] M5: Tests passing — Round-trip serialization tests
Out of Scope
- [ ] Task execution (separate executor plugin/capability)
- [ ] Module discovery (which modules exist)
- [ ] Module argument validation
- [ ] Variable/Jinja2 template resolution
- [ ] Loop expansion
- [ ] Conditional evaluation
Resolved Questions
Union vs flat structure?
- Flat with
is_blockflag
- Flat with
How to handle free-form module args?
shell: echo hellohas args as a string- add
free_form_args:stringfield?
Handler-specific fields?
listenonly applies to handlers- Include in Task and add a is_handler flag in addition to listen (since listen can be empty for handlers and for tasks)
Recommended Implementation Order
Since tasks are a dependency of playbooks:
- First: Implement
flatbuffers/domain/task.fbs - Then: Implement
flatbuffers/domain/playbook.fbs(imports task.fbs) - Then: Implement playbook plugin (parses both)
- Finally: CLI commands for playbooks
Task parsing code lives in the playbook plugin since standalone task files are just arrays of tasks - the same structure as a play's task list.