Pandora Plugin Executor
Generic Plugin Executor for Pandora FMS.
- Introduction
- Compatibility matrix
- Pre requisites
- Parameters
- Manual execution
- Configuration in PandoraFMS
- Agent and modules generated by the plugin
- Shipped templates
- Template reference
- Creating your own templates
- Expression language quick reference
- Worked example: replacing a real plugin
- Template checklist (for humans and AI generators)
Introduction
pandora-plugin_exec is a single Go binary that replaces per-technology plugins. A YAML template describes the work to do:
- Tasks to run — HTTP requests or local command executions.
- Filters (jq or regexp) that extract values from a task's output into variables.
- Agents and modules built from those variables (including arithmetic expressions).
- A transfer method that delivers the resulting data to Pandora FMS.
The executor runs the template once and exits (one-shot). Adding support for a new technology means writing a new YAML template — not a new plugin, not a new language, not a new packaging pipeline. It is meant to be used alongside the Pandora FMS agent.
template.yml ─▶ run tasks ─▶ filter outputs into variables ─▶ render modules ─▶ deliver
-
Version:
0.1.0(pandora-plugin_exec --version).
Audience: humans writing templates and AI agents generating them. Everything needed to write a valid template is in this file.
Out of the box the plugin ships pre-built templates under templates/: learning examples plus drop-in replacements for agent plugins that used to ship with the agent. See Shipped templates.
Compatibility matrix
Only Linux has been exercised so far. Windows is planned but has not been tested yet.
| Platform / runtime | Status |
|---|---|
| Linux amd64 | Tested |
| Linux arm64 | Not established |
| Windows amd64 | Not established |
| macOS | Not established |
The delivered binary is static, so it has no host runtime dependencies.
Pre requisites
To run the binary:
- No Go runtime, no external
jq, and no Perl/Python are required — the binary is static and embeds the jq engine (gojq). -
/bin/shis used to runlocaltask commands, so the commands referenced by the template (df,ss,awk,/procfiles, etc.) must exist on the host.
Per transfer mode:
| Mode | Requirement |
|---|---|
agent_plugin |
A Pandora FMS software agent that can invoke the binary via module_plugin |
tentacle |
The tentacle_client binary (on $PATH, or its path set via transfer.tentacle.binary) and network access to the Pandora FMS server (default port 41121) |
local |
Write permission on the target directory (transfer.local.directory) |
Parameters
The executor has three configuration surfaces:
- CLI flags (this section).
-
Runtime parameters — positional
argsand namedparamspassed after the flags (this section). - The YAML template — tasks, agents/modules and transfer. Full reference in Template reference.
CLI flags
pandora-plugin_exec -t <template.yml> [flags] [args...] [key=value...]
| Flag | Effect |
|---|---|
-t, --template <path> |
Template to execute (required) |
--dry-run |
Print the generated XML to stdout, do not transfer |
--strict |
Abort on the first task failure instead of skipping |
-v, --verbose |
Debug logging to stderr (task results, skips, warnings) |
--version |
Print version and exit |
| Exit code | Meaning |
|---|---|
| 0 | Success |
| 1 | Template parse/validation error |
| 2 | Task failure under --strict |
| 3 | Transfer error |
Flags before arguments: flag parsing stops at the first non-flag token, so flags must precede the arguments (pandora-plugin_exec -t tpl.yml --dry-run sda1, not ... sda1 --dry-run). A dash-leading argument is rejected with a clear error instead of being silently treated as a positional.
Runtime parameters
Tokens left after the flags feed two reserved variables, seeded into the store before any task runs:
-
args— ordered list of positional tokens:{{ args[0] }},{{ len(args) }},for_each: args. -
params— map of named tokens:{{ params.host }},{{ params.threshold + 5 }}.
Parsing rules:
- Each token splits on the first unescaped
=. A=immediately preceded by\is an escaped literal and does not split. - The token is named only if the candidate key (after unescaping
\=→=) matches[A-Za-z_][A-Za-z0-9_]*. Otherwise the whole token is positional.
Every token classifies — there are no error cases. Duplicate named keys: last wins. Numeric-looking values become numbers, so arithmetic works on them. Task variables cannot be named args or params (reserved); for_each: args is fine.
Shell quoting gotcha: bash consumes \= before the executor sees it. Protect the backslash with single quotes ('query\=a=b') or double it (query\\=a=b).
| Token | Becomes |
|---|---|
sda1 |
args[0] = "sda1" |
90 |
args[0] = 90 (number) |
host=db01 |
params.host = "db01" |
threshold=90 |
params.threshold = 90 (number) |
conn=user=a;pw=b |
params.conn = "user=a;pw=b" (only the first = splits) |
'query\=a=b' |
positional "query=a=b" (the key would contain = — not an identifier) |
=foo |
positional "=foo" (empty key) |
k=1 k=2 |
params.k = 2 (last wins) |
Optional parameters
Runtime parameters fail loudly, not silently: referencing an undefined variable does not render an empty string — the module is skipped with a warning, to protect against typos. A bare {{ params.x }} therefore skips the module when x was not passed. To make a parameter optional, pick an explicit pattern:
| Pattern | Effect |
|---|---|
{{ params.missing ?? "default" }} |
Render a default when the key is absent |
when: "'k' in params" |
Include the module only when the key was passed |
{{ len(args) > 1 ? args[1] : "all" }} |
Guard a positional access by count |
Parameters accepted by the shipped templates
| Template | Parameter | Default | Effect |
|---|---|---|---|
templates/df_used.yml |
include |
— | Force-monitor a mount whose fstype is outside the default list (comma-separated exact mount matches) |
templates/df_used.yml |
exclude |
— | Never monitor a mount — exclude always wins over include |
templates/pandora_netusage.yml |
include |
— | Monitor only these interfaces (comma-separated exact names) |
templates/pandora_netusage.yml |
exclude |
— | Never monitor an interface — exclude always wins over include |
templates/pandora_mem_used.yml |
mem_critical |
95 |
Critical threshold for Memory_Used percent |
templates/pandora_mem_used.yml |
swap_critical |
95 |
Critical threshold for Swap_Used percent |
Manual execution
Execution format
pandora-plugin_exec -t <template.yml> [flags] [args...] [key=value...]
Examples
Run a shipped template without sending anything (the pandora-plugin_exec binary is delivered pre-built):
pandora-plugin_exec -t templates/df_used.yml --dry-run
You should see <module> XML blocks on stdout, one pair per mounted filesystem.
Real runs with runtime parameters:
pandora-plugin_exec -t templates/df_used.yml # default fstype allow-list
pandora-plugin_exec -t templates/df_used.yml exclude=/DB,/snap # drop two mounts
pandora-plugin_exec -t templates/df_used.yml include=/mnt/zfs # monitor a zfs mount too
pandora-plugin_exec -t templates/pandora_netusage.yml # every interface
pandora-plugin_exec -t templates/pandora_netusage.yml exclude=lo,docker0 # drop loopback and docker0
pandora-plugin_exec -t templates/pandora_netusage.yml include=eth0,ens33 # only eth0 + ens33
pandora-plugin_exec -t templates/pandora_netusage.yml include=lo exclude=lo # exclude wins → 0 bytes
pandora-plugin_exec -t templates/pandora_mem_used.yml # defaults
pandora-plugin_exec -t templates/pandora_mem_used.yml mem_critical=90 swap_critical=85
Verbose mode
-v / --verbose writes debug logging to stderr, leaving stdout clean for the generated XML. It reports per-task results (stored variables), skipped modules, and warnings:
pandora-plugin_exec -t templates/df_used.yml --dry-run -v
There is no log file or log level; verbose output is a single stderr stream.
Configuration in PandoraFMS
The executor is one-shot: it runs the template once and exits. Scheduling is external.
| Scenario | How |
|---|---|
| As an agent plugin | Template uses transfer.mode: agent_plugin; agent config: module_plugin /usr/bin/pandora-plugin_exec -t /etc/pandora/templates/df_used.yml |
| From cron / server side | Template uses tentacle or local mode; crontab: */5 * * * * pandora-plugin_exec -t /etc/pandora/templates/myapp.yml |
| Running on the Pandora FMS server itself | local mode pointed at the incoming data directory (/var/spool/pandora/data_in) |
| Debugging a template | pandora-plugin_exec -t template.yml --dry-run -v |
There is no wizard or .disco package for this plugin: installation means placing the binary (and templates) where the agent/cron can reach them, then wiring the module_plugin line or crontab entry shown above.
Agent and modules generated by the plugin
Agents. Each agents: block becomes one <agent_data> XML document (in tentacle / local modes). Agent identity comes straight from the template: name, alias, parent_agent_name, description, version, os_name, os_version, timestamp, address, group, interval, agent_mode. In agent_plugin mode no agent header is emitted — only <module> fragments — and with several agents: blocks each module name is prefixed <agent_name> - <module_name> (separator configurable via transfer.module_prefix_separator) so modules cannot collide under the one real agent.
Modules. One module per resolved module block (a for_each block resolves to one module per element). Module type is whatever the template declares (generic_data, generic_data_inc, generic_data_string, async_data, ...) — the executor forwards the module fields to the dataserver unchanged. The full set of supported module fields is listed in Template reference.
A failed task (execution error, non-2xx HTTP status, filter with 0 matches) leaves its variable unset and logs a warning; a module that references an unset variable is skipped with a warning and the remaining modules are still delivered. --strict turns any of the above into an abort (exit code 2).
Shipped templates
The plugin ships with pre-built templates under templates/, split into two groups.
Examples
| Template | Purpose |
|---|---|
templates/http_api_example.yml |
Token-auth HTTP API chaining (login → token → authenticated request) |
templates/local_commands_example.yml |
Local commands as an agent plugin, showing all regexp result shapes |
Replacements for legacy agent plugins
| Template | Replaces |
|---|---|
templates/df_used.yml |
pandora_df_used_go — filesystem used space |
templates/pandora_mem_used.yml |
pandora_mem_used — memory and swap |
templates/pandora_netusage.yml |
pandora_netusage — network usage |
Template reference
A template is a YAML file with three sections: tasks, agents (or top-level modules), and transfer. Parsing is strict: an unknown field name is an error, so typos fail fast.
Minimal complete example
tasks:
- name: disk
type: local
local: { command: "df --output=pcent / | tail -1 | tr -d ' %'" }
variable: disk_pct
agents:
- name: "myhost"
group: "Servers"
interval: 300
modules:
- name: "Disk used"
type: generic_data
data: "{{ disk_pct }}"
unit: "%"
transfer:
mode: tentacle
tentacle: { address: "pandora.example.com" }
tasks: — ordered list of executions
Tasks run sequentially, in declared order. A task may use variables produced by earlier tasks in any of its string fields (chaining: login → token → authenticated request).
| Field | Required | Description |
|---|---|---|
name |
yes | Unique task identifier (used in logs) |
type |
yes | request or local |
request |
if type=request | HTTP request spec (below) |
local |
if type=local | Command spec (below) |
filter |
no | Extraction filter (below). Without it, the trimmed raw output is stored |
variable |
no | Name to store the result under. Must match [A-Za-z_][A-Za-z0-9_]* |
request: (HTTP)
| Field | Required | Default | Description |
|---|---|---|---|
url |
yes | — | Target URL. Supports {{ }} |
method |
no | GET |
GET, POST, PUT, DELETE, PATCH, HEAD |
headers |
no | — | Map of header → value. Values support {{ }} |
body |
no | — | Request body. Supports {{ }} |
timeout |
no | 30 |
Seconds |
skip_tls_verify |
no | false |
Accept invalid TLS certificates |
A response with status outside 2xx is a task failure. Response bodies are capped at 10 MiB.
local: (command)
| Field | Required | Default | Description |
|---|---|---|---|
command |
yes | — | Executed with /bin/sh -c. Supports {{ }}. Shell $ is untouched (awk '{print $5}' works as-is) |
timeout |
no | 30 |
Seconds; the process is killed on expiry |
A non-zero exit code is a task failure (stderr is included in the log).
filter: — extracting values
| Field | Required | Description |
|---|---|---|
type |
yes | jq or regexp |
expression |
yes | The filter expression |
Result cardinality (both filter types): 0 matches → variable unset (task failure semantics) · 1 match → scalar · N matches → array.
jq — the task output must be valid JSON. The expression runs with an embedded jq engine (no jq binary needed). Each value the expression emits is one element: .token → scalar, .items[].name → array, .items[] → array of objects.
regexp — Go RE2 syntax (no backtracking, no lookahead) applied to the raw output. Per match:
| Pattern shape | Each match becomes |
|---|---|
No capture group (cpu\d) |
The full match (string) |
One unnamed group ((\d+)%) |
Capture group 1 (string) |
2+ unnamed groups ((a)(b)(c)) |
Positional array: [[a, b, c], ...] |
Named groups ((?P<mount>...)) |
An object: group name → captured text |
Use (?m) for line-anchored multi-line patterns and (?i) for case-insensitive matching.
Do not mix named and unnamed groups in the same regexp if you want predictable access; the named form exposes only the named fields.
Type coercion: extracted strings that look numeric become numbers (recursively, including object fields), so arithmetic works on them.
Variables and {{ }} expressions
Any string field of tasks (after the producing task), agents, and modules may embed {{ expression }}. Expressions are evaluated with expr-lang against the variable store.
| Expression | Result |
|---|---|
{{ token }} |
Variable value |
{{ 100 - disk_pct }} |
Arithmetic |
{{ names[0] }} |
Array indexing |
{{ value.mount }} |
Object field (inside for_each) |
{{ names[index] }} |
Lockstep parallel array (inside for_each) |
Referencing an undefined variable is an error → the affected module is skipped (or the run aborts under --strict).
agents: — one block per agent
Required unless you use top-level modules: (see agent_plugin mode). Each agent becomes one <agent_data> XML document.
| Field | Required | Description |
|---|---|---|
name |
yes | Agent name |
modules |
yes | List of module blocks (at least one) |
alias |
no | Agent alias |
parent_agent_name |
no | Parent agent |
description |
no | Description |
version |
no | Agent version string |
os_name, os_version |
no | OS identification |
timestamp |
no | Override data timestamp |
address |
no | IP/hostname |
group |
no | Target group |
interval |
no | Seconds (integer) |
agent_mode |
no | Agent mode |
All string fields support {{ }}.
modules: — module blocks
Module blocks live inside an agent, or at the top level (only with transfer.mode: agent_plugin).
Control fields (executor semantics, not sent to Pandora):
| Field | Description |
|---|---|
for_each |
Array variable name. The block expands to one module per element; {{ value }} and {{ index }} become available. A scalar variable iterates as a 1-element array. For unnamed capture groups with 2+ groups, each value is an array so you can use {{ value[0] }}. Need several modules per element? Write several blocks with the same for_each |
when |
Raw boolean expr-lang expression (no {{ }}). False → module (or element) skipped. Inside for_each it is evaluated per element and sees value/index. Example: when: 'not (value.mount matches "^/DB")' |
Data fields — the two required plus the full set accepted by the dataserver. Values may be numbers or strings in YAML; all support {{ }}:
| Field | Required | Description |
|---|---|---|
name |
yes | Module name |
type |
yes | Module type (generic_data, generic_proc, generic_data_string, async_data, ...) |
data |
no | Module value |
description |
no | Description |
unit |
no | Unit label |
interval |
no | Module interval |
tags |
no | Tags |
module_group |
no | Module group |
module_parent, module_parent_unlink |
no | Parent module relation |
min_warning, max_warning, min_critical, max_critical |
no | Numeric thresholds |
min_warning_forced, max_warning_forced, min_critical_forced, max_critical_forced |
no | Forced threshold variants |
str_warning, str_critical |
no | String-match thresholds |
str_warning_forced, str_critical_forced |
no | Forced string thresholds |
warning_inverse, critical_inverse |
no | Invert threshold logic |
min, max |
no | Valid data range |
post_process |
no | Multiplier applied by the server |
disabled |
no | Create disabled |
status |
no | Force status |
timestamp |
no | Override data timestamp |
custom_id |
no | Custom identifier |
critical_instructions, warning_instructions, unknown_instructions |
no | Operator instructions |
quiet |
no | Quiet mode |
min_ff_event, min_ff_event_normal, min_ff_event_warning, min_ff_event_critical |
no | FlipFlop thresholds |
module_ff_interval, ff_type, ff_timeout, each_ff |
no | FlipFlop behavior |
crontab |
no | Cron-style module scheduling |
extra_data |
no | Extra payload |
alert_templates |
no | List of alert template names to bind |
transfer: — delivery
| Field | Required | Description |
|---|---|---|
mode |
yes | tentacle, local or agent_plugin |
tentacle.address |
for tentacle | Pandora server address |
tentacle.port |
no (default 41121) |
Tentacle port |
tentacle.binary |
no | Path to tentacle_client if not in $PATH |
tentacle.extra_args |
no | Extra tentacle client arguments (list) |
local.directory |
for local | Directory where the .data XML file is written |
module_prefix_separator |
no (default " - ") |
Separator for multi-agent prefixing in agent_plugin mode |
| Mode | Output | Use case |
|---|---|---|
tentacle |
Full <agent_data> XML sent via tentacle client |
Remote execution (cron, Discovery) |
local |
Full <agent_data> XML written to a directory |
Running on the Pandora server itself (point it at the incoming dir), or debugging |
agent_plugin |
Only <module> fragments printed to stdout — the real agent adds the header |
Running as module_plugin of a software agent |
agent_plugin specifics:
- Top-level
modules:without anyagents:block is allowed (and only allowed in this mode). - With multiple agents defined, each module name is prefixed
<agent_name> - <module_name>so modules cannot collide under the one real agent.
Creating your own templates
A template is just YAML. Build it top-down: decide what to measure (tasks), how to extract the numbers (filters), which modules to expose (agents/modules), and how to deliver (transfer).
Step 1 — Start from a shipped example
templates/local_commands_example.yml is the shortest complete template and demonstrates all three regexp result shapes. Copy it and replace the tasks:
cp templates/local_commands_example.yml templates/my_template.yml
Step 2 — Write the tasks
Each task runs one command or one HTTP request and stores its (optionally filtered) output in a variable.
tasks:
# A scalar: one value, no filter (trimmed raw output is stored).
- name: uptime_seconds
type: local
local: { command: "awk '{print int($1)}' /proc/uptime" }
variable: uptime
# A scalar via a single-capture regexp.
- name: load_avg
type: local
local: { command: "cat /proc/loadavg" }
filter:
type: regexp
expression: '^(\d+\.\d+)'
variable: load1
# An array of objects via named capture groups.
- name: df
type: local
local: { command: "df -kTP" }
filter:
type: regexp
expression: '(?m)^(?P<fs>\S+)\s+(?P<fstype>\S+)\s+(?P<total>\d+)\s+(?P<used>\d+)\s+\d+\s+(?P<pct>\d+)%\s+(?P<mount>.+)$'
variable: filesystems
Step 3 — Turn variables into modules
A variable that is a scalar feeds one module directly; an array feeds for_each blocks (one module per element).
modules:
- name: "Uptime seconds"
type: generic_data
data: "{{ uptime }}"
- name: "Load average 1m"
type: generic_data
data: "{{ load1 }}"
# One module per filesystem; value.* reads the named capture groups.
- for_each: filesystems
when: 'value.fstype != "tmpfs"'
name: "DiskUsed_{{ value.mount }}"
type: generic_data
data: "{{ value.pct }}"
unit: "%"
Inside a for_each block:
-
{{ value }}is the current element,{{ index }}its 0-based position. - Named groups make each element an object → access fields as
{{ value.mount }}. - 2+ unnamed groups make each element a positional array → access as
{{ value[0] }},{{ value[1] }}, ...; write several blocks with the samefor_eachto emit several modules per element.
Step 4 — Pick the transfer mode
transfer:
mode: agent_plugin # modules-only output; run as a software-agent plugin
transfer:
mode: tentacle # full agent XML to a Pandora server
tentacle: { address: "pandora.example.com" }
transfer:
mode: local # full agent XML written to a directory
local: { directory: "/var/spool/pandora/data_in" }
Step 5 — Validate before shipping
pandora-plugin_exec -t templates/my_template.yml --dry-run -v
--dry-run prints the generated XML without transferring; -v shows which variables were stored and which modules were skipped. The parser is strict, so any typo in a field name fails immediately.
Common pitfalls
-
Flags after arguments don't work. Go flag parsing stops at the first non-flag token: put
--dry-runbefore any positional argument. -
Undefined variables skip the module, they don't render empty. Use
{{ params.x ?? "default" }}orwhen: "'x' in params"for optional parameters. -
filter.expressionis not expr. It is jq or an RE2 regexp depending onfilter.type. expr is only used inside{{ }}and inwhen:. -
Double the backslash in regexp literals inside
when:— expr string literals process escapes, so\\ddelivers\dto the regexp engine. -
RE2 has no lookahead/backtracking — use
when:for exclusions (e.g.when: 'not (value.mount matches "^/DB")').
Expression language quick reference
{{ }} placeholders and when: conditions are expr-lang expressions. Useful built-ins for template authors:
| Category | Built-ins / operators |
|---|---|
| Strings | split(s, sep) · join(list, sep) · upper(s) · lower(s) · trim(s) · replace(s, old, new) · contains(s, sub) · startsWith(s, pre) · endsWith(s, suf) · s matches "re2-pattern" |
| Collections | x in list · len(list) · list[i] · first(list) · last(list) · filter(list, pred) · map(list, expr) · all/any/none(list, pred) · sort(list) · uniq(list) · concat(a, b) |
| Logic & nil | a ?? "default" (nil coalescing) · obj?.field (optional chaining) · cond ? a : b (ternary) · 'key' in map (key presence) |
| Numbers | + - * / % · abs(x) · min/max(a, b) · sum(list) · avg(list) · int(x) · float(x) · string(x) |
| JSON | toJSON(x) · fromJSON(s) |
In filter/map/all/any/none predicates the current element is # (e.g. filter(filesystems, #.pct > 90)).
Do not confuse languages: the task filter.expression is not expr — it is jq (via gojq) or an RE2 regexp, depending on filter.type. expr is only used inside {{ }} and in when:.
Worked example: replacing a real plugin
templates/df_used.yml replaces the pandora_df_used_go agent plugin. The pattern to learn: one execution → array of objects → N modules.
tasks:
- name: df
type: local
local: { command: "df -kTP" }
filter:
type: regexp
# Named groups: each df row becomes {fs, fstype, total, used, pct, mount}
expression: '(?m)^(?P<fs>\S+)\s+(?P<fstype>\S+)\s+(?P<total>\d+)\s+(?P<used>\d+)\s+\d+\s+(?P<pct>\d+)%\s+(?P<mount>.+)$'
variable: filesystems
modules:
- for_each: filesystems
when: >
(value.fstype matches "^(?i:adfs|affs|autofs|btrfs|cifs|coda|coherent|efs|ext\\d?|hfs|hfsplus|hpfs|jfs|minix|msdos|ncpfs|nfs4?|ntfs|proc|qnx4|reiserfs|smbfs|sysv|ubifs|udf|ufs|umsdos|usbfs|vfat|xenix|xfs|xiafs)$"
|| value.mount in split(params.include ?? "", ","))
&& not (value.mount in split(params.exclude ?? "", ","))
name: "DiskUsed_{{ value.mount }}"
type: generic_data
data: "{{ value.pct }}"
unit: "%"
- for_each: filesystems
when: >
(value.fstype matches "^(?i:adfs|affs|autofs|btrfs|cifs|coda|coherent|efs|ext\\d?|hfs|hfsplus|hpfs|jfs|minix|msdos|ncpfs|nfs4?|ntfs|proc|qnx4|reiserfs|smbfs|sysv|ubifs|udf|ufs|umsdos|usbfs|vfat|xenix|xfs|xiafs)$"
|| value.mount in split(params.include ?? "", ","))
&& not (value.mount in split(params.exclude ?? "", ","))
name: "DiskUsed_{{ value.mount }} bytes"
type: generic_data
data: "{{ value.used }}"
unit: "bytes"
transfer:
mode: agent_plugin
The regexp captures all df rows; the default fstype allow-list lives in when: so runtime params can override it. Note the folded scalar (when: >) and the doubled backslash (\\d): expr string literals process escapes, so \\d delivers a verbatim \d to the regexp engine.
Positional captures example
Use this when you want capture order instead of field names:
tasks:
- name: fs
type: local
local:
command: |
printf '/dev/sda1 ext4 40 81 /\n/dev/sdb1 xfs 90 12 /data\n'
filter:
type: regexp
expression: '(?m)^(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(.+)$'
variable: filesystems
modules:
- for_each: filesystems
name: "FS {{ value[0] }}"
type: generic_data
data: "{{ value[3] }}"
unit: "%"
transfer:
mode: agent_plugin
HTTP API example
templates/http_api_example.yml demonstrates token-auth chaining: a login request stores the session token, and later requests embed it via Authorization: "Bearer {{ token }}".
Template checklist (for humans and AI generators)
- Every task has a unique
nameand its block matches itstype(request:xorlocal:). - Every
variablename matches[A-Za-z_][A-Za-z0-9_]*(and is notargs/params). - Tasks are ordered so variables are produced before they are consumed.
- Either
agents:(with ≥1 module each) or top-levelmodules:+agent_pluginmode — never both. - Every module has
nameandtype. -
for_eachreferences a variable that a filter produces as an array; element fields are accessed as{{ value.field }}for named captures or{{ value[0] }}for positional captures. -
when:is a raw expression (no{{ }}); everything else uses{{ }}placeholders. -
transfer.modehas its required options (tentacle.address/local.directory). - Validated with
pandora-plugin_exec -t template.yml --dry-run -vbefore shipping.