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.
Requires Aspect CLI v2026.23.40 or newer.
Adding a tip
Atask_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
| Argument | Type | Notes |
|---|---|---|
ctx | TaskContext | The task the tip is added to. |
id | str | Stable identifier — the kind of tip and its silence key. Re-emitting the same (id, key) replaces the stored tip; id is what --tips:silence=<id> matches. |
severity | str | TIP_IMPORTANT / TIP_WARNING / TIP_SUGGESTION / TIP_INFO. Drives ordering, the emoji/label, and (for important) immunity from silencing. |
title | str | Short headline, interpreted per type. |
body | str | Markdown body, interpreted per type. |
type | str | Interpolation engine — TIP_TEMPLATE_RAW (default), TIP_TEMPLATE_FORMAT, or TIP_TEMPLATE_JINJA2. See Template types. |
key | str | Optional discriminator for same-id tips about different subjects (e.g. one flaky-test tip per label). Empty → a single tip per id. |
vars | dict | Fixed scalar values for FORMAT / JINJA2 interpolation. |
accumulate | dict | List-valued Jinja2 fields that union across re-emits (deduped + sorted). JINJA2 only. |
surfaces | list[str] | Allowlist of surfaces that may render the tip. Empty (default) shows it everywhere. See Surfaces. |
combine_across_tasks | bool | How same-(id, key) tips from sibling tasks combine in the PR summary. See Accumulating tips. |
Tip identity
A tip is identified by the pair(id, key):
idis 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 byidalone, across everykey.keyis an optional discriminator for “same kind of tip, different subject.” For example, a flaky-test tip might useid = "flaky-test"withkey = "//pkg:my_test"so each flaky target gets its own tip, while--tips:silence=flaky-testsilences 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:| Severity | Use for | Silenceable? |
|---|---|---|
TIP_IMPORTANT 🔥 | Something the user really should act on now. | No — the silence list is ignored. |
TIP_WARNING ⚠️ | Something worth attention but not blocking. | Yes |
TIP_SUGGESTION 💡 | An improvement or nicety to consider. | Yes |
TIP_INFO ℹ️ | Informational; FYI. | Yes |
TIP_IMPORTANT ignores the silence list (see
Silencing tips).
Template types
Thetype 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— Starlarkstr.format(**vars);{name}placeholders are replaced fromvars.TIP_TEMPLATE_JINJA2— Jinja2 source rendered againstvars(fixed scalars) plusaccumulate(each name a deduped + sorted list). The source is retained so the tip re-renders whenever itsaccumulatestate grows.
Surfaces
By default a tip shows on every surface. Passsurfaces to restrict it to an allowlist:
| Identifier | Surface |
|---|---|
SURFACE_CLI | The terminal TIP: block. |
SURFACE_GITHUB_STATUS_CHECKS | GitHub check-run summary body. |
SURFACE_BUILDKITE_ANNOTATIONS | Buildkite annotations. |
SURFACE_GITHUB_PR_SUMMARY | The aggregate PR summary comment. |
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— justSURFACE_GITHUB_PR_SUMMARY.SURFACE_ALL— everything (equivalent to leavingsurfacesempty).
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” — useTIP_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
add_tip, before the tip reaches storage.
Verdicts
| Verdict | Effect |
|---|---|
TIP_ACCEPT | Keep the tip unchanged. Chain continues to the next hook. |
TIP_REJECT | Drop the tip entirely. Short-circuits the chain. |
tip_replace(severity=, title=, body=, surfaces=, combine_across_tasks=) | Rewrite the given fields; omit any to keep its prior value. Chain continues with the rewritten tip. |
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
| Field | Type | Notes |
|---|---|---|
task_path | str | Joined <group...> <name> CLI path. |
task_name | str | Just the task name. |
task_group | list[str] | The group prefix. |
id | str | The tip’s stable id. |
key | str | The tip’s discriminator (may be empty). |
severity | str | A TIP_* severity value. |
title | str | Pre-rendered title — empty for JINJA2 tips (rendered after the hook). |
body | str | Pre-rendered body — empty for JINJA2 tips. |
type | str | The template type. |
surfaces | list[str] | The tip’s surface allowlist (empty = all). |
combine_across_tasks | bool | The tip’s cross-task combine policy. |
Examples
Veto a tip by id:.aspect/config.axl
.aspect/config.axl
.aspect/config.axl
Silencing tips
To suppress a non-important tip without writing a hook, append itsid to the Tips feature’s silence list, or pass it on the command line:
.aspect/config.axl
id and points at this knob. Important tips (TIP_IMPORTANT) ignore the silence list — they’re meant to always reach the user.
See also
- How to customize repro & fix suggestions — the sibling accept/reject/replace hook for the
🔁 Reproduce/🛠️ Fixlines. - How to run and define tasks — task fundamentals and the
task()definition surface. - Aspect Extension Language overview — what AXL is and why it’s typed Starlark.

