Skip to main content
Tips are non-fatal, actionable recommendations the Aspect CLI surfaces where the user is already looking — the terminal, GitHub check-run summaries, Buildkite annotations, and the aggregate PR summary comment — so the product is self-documenting. Users shouldn’t have to grep logs for WARN lines or read docs to discover an improvement. A tip has a severity (🔥 important / ⚠️ warning / 💡 suggestion / ℹ️ info), a title and body, and an identity that controls de-duplication and silencing. You add tips with add_tip from any code that has a TaskContext — most commonly a task_update hook in your .aspect/config.axl.

Adding a tip

A task_update handler is the usual place to add a tip. The handler fires repeatedly over a task’s lifetime, but that’s fine: re-emitting the same (id, key) just replaces the stored tip (and the terminal only re-prints when the content actually changed), so a static tip can emit on every update without piling up or spamming:
.aspect/config.axl
add_tip is also a no-op when the Tips feature is disabled (--tips:enabled=false), so emit sites never need to probe for it. If building the tip’s content is expensive, gate the work behind a once-flag. Keep the flag local to config and close over it — use a one-element list so the closure can mutate it (AXL, like Starlark, makes a plain assignment inside a nested function a new local, so a bare bool wouldn’t stick):
.aspect/config.axl

add_tip arguments

Tip identity

A tip is identified by the pair (id, key):
  • id is the kind of tip and the silence key. Two emits with the same (id, key) are the same tip — the later one replaces the stored content (so a task can revise a tip it made earlier). Silencing is always by id alone, across every key.
  • key is an optional discriminator for “same kind of tip, different subject.” For example, a flaky-test tip might use id = "flaky-test" with key = "//pkg:my_test" so each flaky target gets its own tip, while --tips:silence=flaky-test silences them all.

Severity ordering

Tips render most-urgent-first, stable within a bucket by insertion order. Severity is just a priority/visual signal — a tip can be about anything, not necessarily a failure or a feature: Pick the level by how much you want it to stand out. Severity only affects ordering and the emoji/label; silencing is by id (see Silencing tips), independent of severity.

Template types

The type argument selects how title / body are interpreted:
  • TIP_TEMPLATE_RAW (default) — no interpolation. Most tip bodies are literal markdown with {} that must survive verbatim, so this is the safe default.
  • TIP_TEMPLATE_FORMAT — Starlark str.format(**vars); {name} placeholders are replaced from vars.
  • TIP_TEMPLATE_JINJA2 — Jinja2 source rendered against vars (fixed scalars) plus accumulate (each name a deduped + sorted list). The source is retained so the tip re-renders whenever its accumulate state grows.

Surfaces

By default a tip shows on every surface. Pass surfaces to restrict it to an allowlist: Three shorthands cover the common groupings:
  • TASK_SCREENS — every per-task surface (CLI + check run + Buildkite), but not the cross-task PR summary. Use this for per-invocation tips like deep links that don’t belong in a repo-wide rollup.
  • AGGREGATE_SCREENS — just SURFACE_GITHUB_PR_SUMMARY.
  • SURFACE_ALL — everything (equivalent to leaving surfaces empty).
The terminal print is gated on SURFACE_CLI; omit it to keep a tip off the terminal while still rendering it on status surfaces.

Accumulating tips

For a tip that grows as a condition recurs — e.g. “these N scopes were missing across the run” — use TIP_TEMPLATE_JINJA2 with accumulate. Each emit contributes one value per field; repeated emits union them (deduped, insertion-ordered), and the Jinja2 body re-renders against the merged set:
.aspect/config.axl
combine_across_tasks = True unions the accumulated set across every sibling task into a single PR-summary row, instead of the default last-task-wins.

Customizing tips: the tip_suggestion hook

TipsTrait.tip_suggestion lets your config accept, reject, or rewrite any tip before any surface renders it. It’s the same accept/reject/replace shape as the repro_fix_suggestion hook.
.aspect/config.axl
Hooks run in registration order on every add_tip, before the tip reaches storage.

Verdicts

A tip_replace of title / body has no effect on a TIP_TEMPLATE_JINJA2 tip: its templates aren’t rendered until after the hook runs, so scope JINJA2 tips by id / key / type and rewrite policy fields (severity / surfaces / combine_across_tasks) instead.

TipInfo fields

Examples

Veto a tip by id:
.aspect/config.axl
Downgrade a warning to a suggestion:
.aspect/config.axl
Keep a tip off the PR summary by narrowing its surfaces:
.aspect/config.axl

Silencing tips

To suppress a tip without writing a hook, set the Tips feature’s silence list to the ids you want to drop, or pass them on the command line:
.aspect/config.axl
Each rendered tip’s footer names its own id and points at this knob.

See also