Annotations
An annotation is a markdown note attached to a run and shown on its run page. Use one to surface what matters about a run without making someone read the logs: a build summary, a coverage table, lint findings, a link to a deployed preview.
spool annotate --style success --title "Build summary" \ "**3 packages** built, **71 tests** green. Image pushed to registry.example.com."Where they show up
Section titled “Where they show up”The run page in the console shows annotations above the steps, rendered as sanitized markdown (tables, links, lists, code) and sorted by severity with errors first.
A workflow writes the body, and a fork’s pull request can start a
workflow, so the console treats the body as untrusted input. Script,
event handlers and unknown URL schemes are stripped, and so are
<form>, <button> and the style attribute: together those could
paint a convincing sign-in over the page and post what gets typed
elsewhere, with no script at all. The console also serves a
Content-Security-Policy that refuses the same things, so a gap in one is
covered by the other.
Formatting survives. A body generated from tool output keeps its
markdown and inline HTML and loses anything interactive. Images from
other hosts are refused too, because loading one would tell whoever
wrote the annotation that you opened the run; inline a status badge as a
data: URL.
Emitting them
Section titled “Emitting them”spool annotate writes one annotation. The body comes from the
positional argument, or from stdin if omitted:
spool annotate --style info "Plain note"some-tool --report | spool annotate --style warn --title "Report"Flags:
| Flag | Meaning |
|---|---|
--style |
error | warn | info | success (default info) |
--title |
Optional bold heading above the body |
--name NAME |
A stable key. Annotating again with the same name replaces the previous note (or appends with --append) instead of adding a new one |
--append |
With --name, concatenate onto the existing note rather than replacing |
--name keeps one live annotation current through a long pipeline,
such as a deploy note the deploy job overwrites as it progresses,
where plain calls would stack four near-identical notes. Names
starting with spool. are reserved for the engine, and spool annotate
refuses them.
A run holds at most 200 annotations, named ones included; annotating
again under a name the run already has updates that note and always
works. A title or body is at most 64 KiB, and --append is refused once
it would take the note past that.
Notes the engine writes
Section titled “Notes the engine writes”Two notes appear on a run without anyone asking for them, both in the warn style.
agent lost appears when an agent running one of the run’s steps
stops reporting for longer than its lease, and the step goes back on the
queue for another agent. There is one per step; losing another agent on
the same step replaces it. It says the lease lapsed, since from the
server a dead agent and a silent one look the same. The ×N beside the
step counts attempts; the note says why they happened. What the lost
attempt printed is in the run’s raw log (spool logs <run-id> --follow).
This run finished before its fan-out children did appears on a run
that succeeded while activities it submitted with spool activity submit
were still going, because nothing collected them. It names the
unfinished ones (the first five, then a count). Their results were never
recorded, so a failure among them would otherwise go unnoticed.
Where you can call it
Section titled “Where you can call it”spool annotate works in any step that runs on an agent: a steps:
job, a graph shell or code step, and a polyglot script (a script:
job, a workflow: script job, or spool submit --as=script).
It is a side-channel call: it talks to the server but records nothing
in the run’s history. In a script it runs again on every replay pass, so
a run that suspends N times posts the same note N+1 times. In any script
that sleeps or waits, give the call a --name so each pass replaces
the note.
For the same reason it works in a shell step, where the durable
primitives (spool step, sleep, wait signal, activity) are
refused: a shell step runs once and can’t suspend and replay, and an
annotation doesn’t need to. Atomic and durable
activities sets out which
calls fall on which side. See the YAML reference
for the job kinds and Polyglot scripts for the
durable toolkit.
Example: a release pipeline
Section titled “Example: a release pipeline”jobs: build: steps: - name: compile run: | set -eu start=$(date +%s) go build ./... spool annotate --style success --title "Build" \ "Compiled in $(( $(date +%s) - start ))s." - name: coverage run: | set -eu go test -cover ./... > coverage.txt spool annotate --name coverage --style info --title "Coverage" \ "$(printf '```text\n%s\n```' "$(cat coverage.txt)")"