Skip to content

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.

Terminal window
cryo annotate --style success --title "Build summary" \
"**3 crates** compiled, **71 tests** green. Image pushed to registry.9000.dev."

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.

cryo annotate writes one annotation. The body comes from the positional argument, or from stdin if omitted:

Terminal window
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.

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).

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, the workflow: 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 like annotate carry 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.

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)"