Annotations
An annotation is a markdown note attached to a run and shown on the run-detail page. Use them 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.
cryo annotate --style success --title "Build summary" \ "**3 crates** compiled, **71 tests** green. Image pushed to registry.9000.dev."Where they show up
Section titled “Where they show up”The run-detail page renders annotations as sanitized markdown (tables, links, lists, code), sorted by severity with errors first.
An annotation body is written by a workflow, and a workflow can be
started by a fork’s pull request, so the console treats it as untrusted
input. Script, event handlers and unknown URL schemes are stripped, and
so are <form>, <button> and the style attribute - between them
those are enough to paint a convincing sign-in over the page and post
what gets typed somewhere else, without any script involved. The
console also serves a Content-Security-Policy that refuses the same
things independently, so a gap in one is not a gap in both.
What survives is the formatting the panel is for. If you are generating
a body from tool output, expect markdown and inline HTML to render and
anything interactive to disappear. An image from another host is
refused too, so a status badge has to be inlined as a data: URL or
served from the console’s own origin - the alternative is that opening
a run tells whoever wrote the annotation that you opened it.
Emitting them
Section titled “Emitting them”cryo annotate writes one annotation. The body comes from the
positional argument, or from stdin if omitted:
cryo annotate --style info "Plain note"some-tool --report | cryo annotate --style warn --title "Report"Flags:
| Flag | Meaning |
|---|---|
--style |
error | warn | info | success (default info) |
--title |
Optional bold heading above the body |
--context NAME |
A stable key. Re-annotating the same context replaces the previous note (or appends with --append) instead of adding a new one |
--append |
With --context, concatenate onto the existing note rather than replacing |
--context is how a long pipeline keeps a single live annotation
current - e.g. a deploy context the deploy job overwrites as it
progresses, rather than stacking four near-identical notes.
The one the engine writes
Section titled “The one the engine writes”A run gets an annotation it did not ask for when an agent running one of
its jobs goes quiet: the agent stopped reporting for longer than its
lease, so the job goes back on the queue for somebody else. It reads
agent lost, in the warn style, one per job. Losing another agent for
the same job replaces the note rather than stacking or growing it.
It says the lease lapsed rather than that the agent died, because from here those look the same - a machine that went away and one that was too busy to check in both stop reporting.
The run’s history does not otherwise say this happened. The ×N on the
job’s row counts the runs of its body, but only once the job ends, and
it does not say why any of them started again; the note is live and
names the cause. What the lost run printed is in the run’s raw log
(cryo logs <run-id> --follow).
Where you can call it
Section titled “Where you can call it”cryo annotate is a side-channel call: it talks to the server but
does not create a durable boundary. The flip side: in a polyglot
script it runs again on every replay pass, so a run that suspends N
times posts the same note N+1 times. On any run that sleeps or
waits, give the call a --context - re-annotating a context
replaces the note instead of stacking duplicates. It works from
both kinds of cryosleep job:
- Polyglot scripts (
script_run, theworkflow:job kind) - alongside the durable primitives (cryo step,sleep,wait-signal,activity-*). - YAML shell steps (
steps:jobs) - where the durable primitives are not available. A shell step runs once, atomically; it can’t suspend and replay, so durable calls would silently lose their guarantees and the agent rejects them with an error. Side-channel calls likeannotatecarry no such constraint, so they’re allowed.
That split is the whole rule: durable primitives need an activity that can suspend; annotations don’t. Atomic and durable activities sets out which calls fall on which side and why. 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 cargo build --release cryo annotate --style success --title "Build" \ "Compiled in $(cat .build-time)s." - name: coverage run: | set -eu cryo annotate --context coverage --style info \ "$(cargo llvm-cov --summary-only --markdown)"