drumbeat-automation-authoring
$
npx mdskill add microsoft/amplifier-drumbeat/drumbeat-automation-authoringAn automation is **one markdown file**. The YAML frontmatter is the entire machine surface — *when it runs, whether anyone hears about it, and what it does*. The markdown body is a human-facing description and is **never parsed for execution**. This skill is the how-to; the frozen contract is [`contracts/automation-file.v1.md`](https://github.com/microsoft/amplifier-drumbeat/blob/main/contracts/automation-file.v1.md) and the full reference is [`docs/AUTOMATIONS.md`](https://github.com/microsoft/amplifier-drumbeat/blob/main/docs/AUTOMATIONS.md).
SKILL.md
.github/skills/drumbeat-automation-authoringView on GitHub ↗
---
name: drumbeat-automation-authoring
description: >-
Author and validate drumbeat automation files — the closed-vocabulary YAML
frontmatter contract. Covers the registered top-level keys, structured `steps:`
objects ({id, prompt, label?} — steps live in the frontmatter, never the
markdown body), the schedule grammar, `notify:` policy with its
NOTHING_TO_REPORT and URGENT: sentinels, `conversation:` lifecycles
(continuous/fresh/daily), the `requires:` pre-run gate and `inject:` state
turns, `agent_config:` host-config overlays and the workspace baseline, loud
refusal semantics, the guidance-file loop convention, and how to write steps
that hold up unattended — with one complete worked example. Use when writing,
editing, reviewing, or debugging a drumbeat `automations/*.md` file.
metadata:
project: drumbeat
contract: https://github.com/microsoft/amplifier-drumbeat/blob/main/contracts/automation-file.v1.md
---
# Authoring drumbeat automations
An automation is **one markdown file**. The YAML frontmatter is the entire
machine surface — *when it runs, whether anyone hears about it, and what it
does*. The markdown body is a human-facing description and is **never parsed for
execution**. This skill is the how-to; the frozen contract is
[`contracts/automation-file.v1.md`](https://github.com/microsoft/amplifier-drumbeat/blob/main/contracts/automation-file.v1.md)
and the full reference is
[`docs/AUTOMATIONS.md`](https://github.com/microsoft/amplifier-drumbeat/blob/main/docs/AUTOMATIONS.md).
**Two load-bearing rules.** (1) The frontmatter vocabulary is **closed**: every
key is registered, and an unknown or retired key is refused loudly with a remedy
at save/load — never ignored. (2) **Steps are structured frontmatter data**,
`{id, prompt, label?}` objects — a numbered list left in the body is the retired
shape and is refused with a pointer to the contract.
The filename stem is the automation's **slug** (`email-check.md` →
`email-check`), used in run-artifact paths and API routes.
## 1 · The frontmatter (closed vocabulary)
| Key | Required | Value |
|---|---|---|
| `name` | yes | Human-readable name, used in reports/notifications |
| `enabled` | no (default `true`) | Boolean. `false` = parsed and listed, never scheduled |
| `trigger` | yes | Mapping; see §3 |
| `steps` | yes | Ordered list of `{id, prompt, label?}` objects; see §2 |
| `notify` | no (default `auto`) | `always` · `auto` · `urgent-only` · `never`; see §4 |
| `requires` | no (default `[]`) | List of tool names and/or workspace-relative file paths; see §5 |
| `inject` | no | List of `{argv, label}`; see §6 |
| `conversation` | no (default `continuous`) | `continuous` · `fresh` · `daily`; see §7 |
| `guidance_delivery` | no (default `reference`) | `reference` · `inline` — how required guidance FILES reach the agent |
| `priority` | no (default `normal`) | `high` · `normal` — dispatch order among automations due at the same tick. Ordering only: it changes who waits, never how many runs complete |
| `agent_config` | no | Per-automation host-config overlay (provider, model, mcp, skills, debug); see §8 |
There is **no `session:` key** — session pins are engine state in
`<data-dir>/session_pins.json`, and nothing writes to your file. Every key not
in this table is refused at parse time.
## 2 · Steps — structured, minimal, one concern each
`steps:` is a **required, ordered list**. Each step object carries exactly three
keys and no others:
| Key | Required | Value |
|---|---|---|
| `id` | yes | A slug (lowercase letters, digits, single hyphens), **unique within the file**. Identity, not control flow — it appears in run records and survives an edit to the prompt |
| `prompt` | yes | Non-empty text; the entirety of the step's behavior, fed as one sequential agent turn |
| `label` | no | Human display name; carries no behavior |
Steps execute in array order, one agent turn per step, all within the
automation's conversation. **A step is judgment (prompt) plus identity (id) and
nothing operational** — scheduling, notify policy, conversation lifecycle, and
agent config are whole-automation concerns, never per-step. There is no
branching, looping, or variables: write a conditional as prose in the prompt and
let the agent apply judgment.
An unknown step key, a missing or duplicate `id`, or an empty `prompt` is refused
loudly. The rotation fingerprint (see §7) covers the ordered step **prompts**
only — editing an `id`, a `label`, or surrounding frontmatter does not abandon
the conversation.
## 3 · Triggers and the schedule grammar
```yaml
trigger:
type: schedule
expression: every 30 minutes
```
| `type` | Meaning |
|---|---|
| `schedule` | Fires on `expression` (**required**) |
| `manual` | Never fires on its own; run via API or CLI |
| `event` | **Reserved** — refuses to load when `enabled: true` (it would validate then never run) |
Schedule expressions are free text in one of these forms:
```
every N minutes every minute every N hours every hour
daily at HH:MM every day at HH:MM
```
`HH:MM` is a 24-hour clock in the **server's local timezone**, recomputed each
evaluation (a DST shift does not silently move your 07:00). Interval forms fire
relative to the last run's finish, not a wall-clock grid. An unparseable
expression skips that automation with a loud log line every tick — it never
crashes the scheduler or silently disables itself.
## 4 · Notify policy and its sentinels
The engine evaluates the policy and emits a **verdict with a reason**; it never
delivers anything itself (a consuming service does).
| Value | Behavior |
|---|---|
| `always` | Final reply always delivered; no judgment turn |
| `auto` | An extra judgment turn decides whether there is anything worth saying |
| `urgent-only` | Identical work/judgment to `auto`, but delivery only if the reply carries an `URGENT:` marker |
| `never` | Never delivered; the run and artifacts still exist |
- **`auto` uses the `NOTHING_TO_REPORT` sentinel.** After the last step the
engine appends a turn asking whether anything is worth surfacing; a reply of
exactly `NOTHING_TO_REPORT` withholds the run with that gate recorded. **State
the sentinel in your own step text** (don't rely on a guidance file that may be
pruned), and **say that a quiet run is a success** — automations that treat
silence as failure learn to manufacture findings.
- **`urgent-only` uses the `URGENT:` marker.** Delivery happens only if the final
reply carries `URGENT: <one-line reason>` (tolerant of surrounding markup like
`**URGENT: …**`). Write into the steps that *the work is never optional* (the
report is still saved) and that *most passes should not carry the marker*.
`NOTHING_TO_REPORT` (agent, inside a turn) and `INJECT_IDLE` (tool, on stdout —
see §6) are deliberately different tokens; never swap them.
## 5 · `requires:` — the pre-run gate
Each entry is a **tool name** or a **workspace-relative file path**, checked
*before step 1*. A file must exist and is injected **verbatim, re-read every
run** (edit it and the next run sees the new text — no restart, no cache). A tool
must resolve on the constructed turn PATH, and its pack card is injected
verbatim. **An unsatisfied requirement aborts the run** with a reasoned record —
it does not run your steps with a missing tool.
**Requiring the guidance file a step says to "load" is not optional.** A step
that says "load and follow `guidance/EMAIL.md`" without a matching `requires:`
entry produces a run where the agent proceeds without the policy and nothing says
so.
## 6 · `inject:` — durable state on every run
An `inject:` entry runs a tool before step 1 and turns its **stdout into a
turn** — how consumer-owned state reaches every run mechanically.
```yaml
inject:
- argv: ["example-cli", "state"]
label: "current state"
```
Classification order is fixed: **timeout → exit code → stdout.** Times out or
exits non-zero → **aborts the run**, voiced. Exits 0 with stdout byte-exactly
`INJECT_IDLE` → injects nothing, run proceeds (a reasoned `inject_skipped` event
is written). Exits 0 with bare-empty stdout → **aborts, loud**. Anything else →
injects stdout verbatim. (Writing the tool? See the sibling
`drumbeat-drumpack-authoring` skill.)
## 7 · Conversation lifecycle (`conversation:`)
By default an automation keeps **one conversation forever**, resuming it every
run — right for a check that should remember what it saw last time.
| Value | Meaning |
|---|---|
| `continuous` | **Default.** One conversation, resumed each run. Abandoned only on a health signal — a provider context-ceiling hit or a change to the step prompts |
| `fresh` | A **new conversation every run**; two runs share no memory |
| `daily` | **One conversation per local calendar day**; first run after local midnight starts fresh, later runs that day resume it |
Any other value is refused at parse time. Rotation is identical across triggers:
the pin is cleared, a line is written to `<data-dir>/session_rotations.jsonl`, and
a `session_rotated` event is emitted — whether caused by a ceiling hit, a steps
rewrite, or a `fresh`/`daily` boundary. Rotation never deletes a session.
Note `conversation:` (does this run reuse the last conversation?) is independent
of the `daily at HH:MM` *schedule* (when a run fires).
## 8 · `agent_config:` — per-automation host config
`agent_config:` shapes the single agent config a turn runs under, for this
automation's turns only. The top-level vocabulary is **closed to
`provider · mcp · skills`**; anything else is refused, and four names are
refused by name with their own reason: `debug` (the library exposes turn events,
not wire payloads, so there is nothing behind the key), `providers` (the library
ships every provider in-process and selects one by id, so a catalog selects
nothing), `approval`, and `allowProtocolSkew`.
```yaml
agent_config:
provider:
module: openai # provider ID; omit to keep the library's default
config:
model_class: fast # fast | standard -- resolved to a concrete model
reasoning_effort: high # minimal | low | medium | high | xhigh
```
**Prefer `model_class:` to a literal `default_model:`.** Model ids rotate, tiers
don't: `fast`/`standard` resolve at materialization per `provider.module`
(anthropic → `claude-haiku-4-5-20251001` / `claude-sonnet-4-6`; openai →
`gpt-5.6-luna` / `gpt-5.6-terra`), overridable per workspace in
`agent-config.yaml`'s `models.classes:` table. An explicit `default_model` wins
and drumbeat WARNS naming the shadowed `model_class`; a `model_class` with no
`provider.module` to resolve against, or an unknown class value, is refused.
`reasoning_effort` is validated at load against the set above, and is refused
outright when no `provider.module` scopes it. A model on the deny-list (`models.deny:`, default `["gpt-5.6-sol"]`)
makes the automation a config-lint failure, so it never runs at all — see
docs/AUTOMATIONS.md §10.
The engine resolves ONE config per turn by merging up to three layers,
**lowest precedence first**: (1) the workspace baseline `agent-config.yaml`
`default:`; (2) a named `profile` (interactive/API turns only); (3) this
automation's `agent_config:`. Merge rules: two mappings recurse; a scalar or
list **replaces** wholesale; a `null` **anywhere is refused loudly** (omit the
key instead). An operator's own `$AMPLIFIER_AGENT_CONFIG` file is not a layer —
it speaks the agent library's own five-key vocabulary and is folded in as the
base of the derived host config, where drumbeat's per-automation policy wins key
by key. `provider.module` carries a provider **ID** (`openai`, `anthropic`,
`azure-openai`, `gemini`, `ollama`, `vllm`, `github-copilot`), and
`reasoning_effort` reaches the provider request only through that derived host
config, scoped under the provider id.
**Credentials are refused anywhere in the block** — any `api_key` / `apiKey` /
`token` / `secret` / `authorization` at any depth fails loud and names the path.
Credentials belong in the engine's **environment**, never a config file. This is
the one rule to memorize. Changing the **provider** rotates the pinned session
automatically (a resumed conversation carries provider-specific state); changing
only the model does not.
## 9 · The guidance-file loop (a convention, not schema)
The pattern that turns a static automation into one that improves: a first step
loads a guidance file, and a final step prunes/updates it.
```yaml
requires:
- guidance/EMAIL.md
steps:
- id: load-guidance
prompt: Load and follow guidance/EMAIL.md.
# … the work …
- id: update-guidance
prompt: >-
Update guidance/EMAIL.md with anything durable you learned. This file is
injected verbatim every run and cannot grow unbounded — use judgement about
when to revise, prune, or consolidate.
```
The **pruning clause is not optional** — without it the file grows until it
crowds out everything else in the context window. Put criteria (judgment) in the
guidance file where they can grow; keep the step pointed at the judgment, not a
hard-coded threshold. Cross-domain learnings go in an `ATTENTION.md`; domain
files stay domain-specific. Your workspace's `guidance/` is yours — there is no
overlay mechanism, and none is coming; a `git`-tracked workspace is the history
and restore path.
## Writing steps that hold up
- **One concern per step** — the step boundary is the unit you will edit later.
- **State the negative space** — the single highest-value sentence is often what
*not* to do: "This is a read-only run: do NOT mark anything read, do NOT send,
do NOT edit files."
- **Demand an honest failure** — "If some part of this cannot be carried out with
the tools you have, say so explicitly rather than approximating." "Nothing
found" and "could not look" are different answers.
- **Recording is not telling** — if the run writes to a ledger, say explicitly
that writing the record is not the same as surfacing it.
- **Ask for judgment, not a threshold** — thresholds need editing every time
reality shifts; judgment plus a guidance file grows.
## Complete worked example
```markdown
---
automation:
name: Email Check
enabled: false # exemplars ship disabled; flip to true to run
trigger:
type: schedule
expression: every 45 minutes
notify: auto
requires: # tool names are placeholders — use your packs'
- mail-cli
- guidance/EMAIL.md
inject:
- argv: ["items-cli", "state"]
label: "open items"
steps:
- id: load-guidance
label: Load guidance
prompt: Load and follow guidance/EMAIL.md as written — no override.
- id: check-inbox
prompt: >-
Check the inbox for mail new or newly relevant since your last check in
this conversation, and tell me what needs my attention using the triage
rules in guidance/EMAIL.md. Check each listing's own completeness field;
if a folder or page could not be read, report it as a coverage gap rather
than treating it as empty. This is a read-only run: do NOT mark anything
read, do NOT draft, do NOT send. If this run has nothing worth surfacing,
reply with exactly NOTHING_TO_REPORT — a quiet run is a success.
- id: update-guidance
prompt: >-
Update guidance/EMAIL.md with anything durable you learned; prune or
consolidate rather than only appending. If some part of this cannot be
carried out with the tools you have, say so explicitly.
---
Triages the inbox against your guidance. This body is a human-facing description
only; the steps above are what runs.
```
## Validate before you trust it
Save-time validation refuses an invalid file loudly. To check without a running
engine, the engine's own `validate_automation_content()` is the conformance
surface (rejects unknown top-level/step keys, missing/duplicate step `id`, empty
`prompt`, missing `steps`, and steps left in the body). Prefer copying a working
exemplar from
[`examples/automations/`](https://github.com/microsoft/amplifier-drumbeat/tree/main/examples/automations)
and editing it over starting from scratch.
More from microsoft/amplifier-drumbeat
| Skill | Description | |
|---|---|---|
| drumbeat-drumpack-authoring | >- | |
| drumbeat-operations | >- |