agentleFS
PostHogKnown publisherPostHog/posthog-foss/.agents/skills/announcing-behavior-changes/SKILL.md

announcing-behavior-changes

Skill727 starsChanged yesterdayMITView on GitHub

Decides whether a behavior-changing fix needs an in-app notice, then builds one that reaches only the affected users and can be removed later. Use when a change alters what an existing user sees without them doing anything — a metric moves, a chart shifts, a count drops, a date range resolves differently, a matcher matches differently — and when adding, reviewing, or removing such a notice. Trigger terms: behavior change, breaking change, semantics change, "results may differ", change notice, deprecation banner, migration banner. Carries the gate (narrow to the affected users with a tested predicate, or do not ship a notice at all), the pattern from `SqlInsightDateFilterNotice`, where to anchor the notice, and the flag-based removal path. Not for new features (use the changelog), not for permanent per-object warnings computed by the backend, and not for the wording itself (see `/writing-user-facing-copy`).

We split this skill into the rules it gives an agent

10 rules. About 0 are common, 7 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈7

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    A user who does nothing sees something different: Not a new feature, not a change they opted into behind a flag.

    line 23
  2. Distinctive:

    They cannot work out why from the screen: If the UI already explains it, the UI is the notice.

    line 24
  3. Distinctive:

    Write the predicate first.

    line 30
  4. Distinctive:

    When that happens, pick a different mechanism instead of widening the blast radius:

    line 32
  5. Very specific:

    Use LemonBanner — do not hand-roll.

    line 88
  6. Very specific:

    Prefer one shared render site over many.

    line 94
  7. Distinctive:

    Put the date in a comment above the component, and repeat it in the flag's description in PostHog so it is visible to whoever audits flags …

    line 100
  8. Distinctive:

    If you are reading this because notices have piled up, that is the gap to close, and it is a better investment than adding features to the …

    line 110
  9. Distinctive:

    Do not apologize, and do not call it a fix or an improvement.

    line 120
  10. Very specific:

    Do not test the component's rendering.

    line 127

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Announcing behavior changes

Some fixes correct wrong behavior but change what a user sees. The number moves, the chart shifts, the count drops. Nothing is broken, but from the user's side it is indistinguishable from a regression, and they have no way to find out why.

An in-app change notice closes that gap: it appears where the user sees the different result, says what changed, and then goes away.

The reference implementation is frontend/src/queries/nodes/DataVisualization/Components/SqlInsightDateFilterNotice.tsx, shipped in commit 76f2905222a alongside the backend change it explains. Read it before writing a new one — it is 60 lines and it is the whole pattern.

First: does this change need a notice?

Most changes do not. A notice that fires for people it does not concern is worse than no notice, because it teaches everyone to dismiss banners without reading them. Ship one only when all four hold.

  1. A user who does nothing sees something different. Not a new feature, not a change they opted into behind a flag.
  2. They cannot work out why from the screen. If the UI already explains it, the UI is the notice.
  3. The difference is big enough to notice. A rounding change in the fourth decimal is not.
  4. You can identify who is affected, in code. See below — this is the one that usually fails.

The narrowing test

Write the predicate first. If you cannot express "this user is affected" as a function of state the client already has, you are about to show a banner to everyone about something that concerns a minority.

When that happens, pick a different mechanism instead of widening the blast radius:

SituationUse instead
Only the backend can tell who is affectedA per-object field on the API response, rendered where that object is shown. Action.selector_warning (#80653) is the model.
The condition is permanent, not a one-time transitionSame — a computed warning, not a time-boxed notice.
Everyone is affected but the change is minorChangelog only.
The old behavior was plainly broken and the new one is self-evidently rightNothing. An error that stops happening needs no announcement.

The pattern

Five pieces. All of them ship in the same PR as the behavior change, so a reviewer sees the change and its explanation together, and the notice cannot be forgotten once the change is out.

1. An exported, tested predicate

Mirror the backend rule in a named function, and say in a comment which backend code it mirrors so the two can be kept honest.

/** Whether the SQL date filter resolution fix can produce different results for this query.
 * Mirrors the affected shapes of ReplaceFilters in posthog/hogql/filters.py. */
export function isAffectedByDateFilterResolutionChange(source: HogQLQuery): boolean {

Export it separately from the component so it can be unit tested without rendering.

2. A feature flag

Declare it in frontend/src/lib/constants.tsx with an owner comment, in the same style as the neighbours:

SQL_INSIGHT_DATE_FILTER_NOTICE: 'sql-insight-date-filter-notice', // owner: #team-product-analytics, gates the notice on SQL insights affected by the date filter resolution fix

The flag does three jobs: it lets you roll the notice out gradually, it lets you turn it off without a deploy if the copy is wrong or the predicate is too broad, and it is the handle you retire the notice by when it has served its purpose.

3. A component that returns null by default

Both gates, flag first — it is the cheaper check and the kill switch:

export function SqlInsightDateFilterNotice({ source }: { source: HogQLQuery }): JSX.Element | null {
  const { featureFlags } = useValues(featureFlagLogic)

  if (!featureFlags[FEATURE_FLAGS.SQL_INSIGHT_DATE_FILTER_NOTICE] || !isAffectedByDateFilterResolutionChange(source)) {
    return null
  }

  return (
    <LemonBanner type="info" dismissKey="sql-insight-date-filter-notice">
      Date filters on SQL insights now match other insights: relative ranges start at midnight, and open-ended ranges
      include all of today but nothing after. Results may differ slightly from before.
    </LemonBanner>
  )
}

Use LemonBanner — do not hand-roll. type="info", because nothing is wrong. dismissKey gives permanent per-user dismissal through lemonBannerLogic's persist: true reducer, at no cost. Keep the key stable and equal to the flag key; changing it re-shows the notice to everyone who already dismissed it.

4. An anchor where the result appears

Render it next to the thing that looks different, not next to the control that changed. The exemplar sits above the visualization in DataVisualization.tsx, not on the date filter, because the chart is what the user is staring at.

Prefer one shared render site over many. A notice about relative date ranges placed in frontend/src/lib/components/DateFilter/DateFilter.tsx reaches insights, dashboards, web analytics, session replay, error tracking, and AI observability at once, because they all render that component.

5. A removal date

The exemplar is missing this, and it is the reason the repo still carries WebAnalyticsFiltersV2MigrationBanner.tsx (added January 2026, untouched since) and SamplingDeprecationNotice.tsx. Nobody deletes these.

Put the date in a comment above the component, and repeat it in the flag's description in PostHog so it is visible to whoever audits flags later:

// Remove after 2026-11-01, once affected users have had a full quarter to see it.

Be aware of what this does and does not buy you. Turning the flag off kills the notice immediately and without a deploy, so the user-visible half of removal is solved. Deleting the code is not: nothing in CI or in any scheduled job currently checks these dates, and the repo has no automated stale-flag sweep. The date is a note to a human.

So take the last step yourself. When you turn the flag off, open the follow-up in the same sitting and delete the component, its test, the flag constant, and the render site together. Do not point /cleaning-up-stale-feature-flags at a notice flag: that skill removes the flag check and keeps the enabled path, and here the enabled path is the banner, so the notice would render for everyone permanently. Delete the code yourself, and check that the component, its test, the flag constant, and the render site all go.

If you are reading this because notices have piled up, that is the gap to close, and it is a better investment than adding features to the notices themselves.

Writing the copy

Invoke /writing-user-facing-copy for the voice rules. The shape that works for this genre is two clauses: what changed, concretely, then that results may differ.

Date filters on SQL insights now match other insights: relative ranges start at midnight, and open-ended ranges include all of today but nothing after. Results may differ slightly from before.

  • Describe the new behavior in terms of what the user sees, not the internals. "Relative ranges start at midnight", not "date_from snaps via relative_date_parse".
  • Say plainly that numbers may differ. That sentence is the whole point — it is what stops someone filing a bug.
  • Do not apologize, and do not call it a fix or an improvement. Both editorialize, and "we fixed a bug" implies their old numbers were worthless, which is usually more alarming than the truth.
  • Two sentences. If it needs more, link to docs.

Testing

The predicate gets a parameterized unit test covering affected and unaffected cases — a wrong predicate means either silence for the people who needed telling, or a banner for everyone else. SqlInsightDateFilterNotice.test.ts is the model: it.each over [name, input, expected], with comments grouping the cases by the rule they exercise.

Do not test the component's rendering. The flag check and LemonBanner are both already covered; per /writing-tests, that test catches no realistic regression.

Checklist

  • The four gate conditions hold, and the predicate narrows to the affected users
  • Predicate exported, commented with the backend code it mirrors, and unit tested both ways
  • Flag declared in lib/constants.tsx with an owner comment
  • Component returns null unless flag and predicate both pass
  • LemonBanner type="info" with a dismissKey equal to the flag key
  • Anchored where the changed result appears, at the most shared render site available
  • // Remove after YYYY-MM-DD comment above the component
  • Shipped in the same PR as the behavior change it explains

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.