- Replace deprecated /v1/saml_connections with /v1/enterprise_connections (modern path); note paid plan SAML feature gate
- Custom roles: use POST /v1/organization_roles to create (was wrongly POST /v1/role_sets/.../roles which only adds existing roles to a set)
- Custom permissions: show two-step flow (POST /v1/organization_permissions to create, then POST /v1/organization_roles/.../permissions/... to attach)
Path-agnostic was too sterile. Restore dual-path mentions inline (Dashboard URL or clerk config patch) so references are self-contained, without looping the agent back to SKILL.md.
References should expand patterns, not redirect back to SKILL.md or prescribe setup paths. Strip Dashboard/CLI path mentions from b2b-patterns and b2c-patterns; SKILL.md owns setup, references focus on plan-type invariants and pattern usage.
Brief 'or via clerk config patch, see SKILL.md → Agent-first' pointers added inline where references currently say 'create plans in Dashboard'. No duplication of the full sidebar — references stay focused on patterns, SKILL.md owns the CLI flow.
Skills should be agnostic of internal Clerk team tooling. Strip the AIE-823 link and 'when the wrapper ships' future-state framing from the Notes block; keep only what works today.
- Update STOP callout to show two paths (Dashboard or clerk config patch with billing.user_enabled/organization_enabled)
- Update Quick Start step 1 to mention CLI alternative
- Add 'Enable Billing via CLI' subsection with PATCH example showing auto-creation of free_user/free_org default plans
- Remove stale 'Enable Billing toggle is Dashboard-only' caveat from Notes
- Note that top-level features manipulation and plan-feature attachments are fully supported via PLAPI billing config handler
- New 'Agent-first: Programmatic billing config' section showing clerk config pull/patch flow + raw clerk api PATCH for full control
- Update STOP callout to clarify CLI can edit plans/features once billing is enabled (Dashboard-only toggle remains for the master switch)
- Note that subscription lifecycle still flows through PricingTable + webhooks
- Reference AIE-823 for the future friendly wrapper swap
Closes AIE-930
- Replace custom AccountBillingPage with <UserProfile /> (B2C), OrgBillingPage with <OrganizationProfile /> (B2B); built-in components own plan switching, cancellations, payment methods
- Use 'org:' slug prefix for org plan checks (has({ plan: 'org:team' })) to disambiguate from user plans in code
- Lead with has({ feature }) over has({ plan }) in Tiered Feature Gating examples; features can move between plans without code deploy
- Remove PricingTable Server Component claim (it is client-side)
- Remove 'metered usage' from core skill router (not shipped)
- Remove Free Plan Fallback section (incorrect concept)
- Strip Stripe leakage from non-setup contexts; keep setup mention and event-name mapping table
- Per-Seat -> Seat-based naming in core router and B2B section heading
- Dedupe seat-limit content between SKILL.md and b2b-patterns.md
- Remove confusing experimental-subpath/underscore notes
- Remove anti-tip about /billing/features 404
- Drop em dashes throughout
Replaces the hosted Clerk logo PNG with a pixel-art SVG bundled in the
repo. The new asset matches the agentic vibe of the skills repo and
removes the external CDN dependency in the README header.
Skill-request issue template has zero usage. Killing it to keep the
repo lean. Skills get added by maintainers based on actual demand
signal, not GitHub issues.
The original line claimed picking the wrong mode causes PricingTable,
OrganizationSwitcher, and personal-account flows to "break silently" —
that overstates what actually happens. In Membership required mode,
signed-in users are routed through the choose-organization task and
personal accounts are disabled by design, not broken. Rewrote to
describe what each mode does and when to pick optional, without the
"break silently" framing.
The old row claimed "<PricingTable /> empty on B2C when Orgs are
enabled | Membership required mode disables personal accounts" — but
if Membership required is on, signed-in users are routed through
choose-organization and never land in a "user without active org"
state to begin with, so the empty-render framing was wrong.
What the Membership mode actually breaks for a billing app is B2C +
B2B coexistence: personal-plan subscriptions can't happen because
personal accounts are disabled by default (since 2025-08-22). Row
rewritten to describe that path specifically, not a PricingTable
render state.
- Add subscriptionItem.expired to the event names table in SKILL.md
and the catalog list in references/billing-webhooks.md so handlers
can branch on it.
- Rename CLERK_WEBHOOK_SECRET to CLERK_WEBHOOK_SIGNING_SECRET in the
3 places the skill referenced it (frontmatter compatibility, Error
Signatures row, billing-webhooks.md Key Rules). The canonical env
var read by verifyWebhook across all SDK packages is
CLERK_WEBHOOK_SIGNING_SECRET; the shorter form never matched and
would produce 'Missing webhook signing secret' at runtime.
SKILL.md had three overlapping tables covering the same errors (Error
Signatures, Debugging has() Failures, Common Pitfalls) and duplicated
the full 70-line webhook handler that already lives in
references/billing-webhooks.md. The main file carried maintenance weight
without earning it.
- Delete 'Debugging has() Failures' section — subsumed by Error Signatures
- Delete 'Common Pitfalls' table — subsumed by Error Signatures
- Trim 'Checkout Flows' — removed duplicate <PricingTable /> example
already shown in §1
- Trim §9 webhook handler from 4 events + middleware snippet down to
one anchoring event; point to references/billing-webhooks.md for the
full template, TS types, middleware setup, and status table
- Add 'What Do You Need?' task-to-reference routing table mirroring
clerk-nextjs-patterns / clerk-orgs convention, so agents know which
reference to read for which concern without scrolling to the index
SKILL.md: 519 -> 450 lines. No information loss; everything pruned
had a canonical home in Error Signatures or references/.
The skill was using Stripe-style 'per-seat billing' language: 'Clerk
auto-increments seats per org member', 'Clerk manages per-seat pricing
internally', etc. That's not the Clerk Billing model.
Verified against clerk_go source + public docs:
- Default B2B billing = seat-LIMIT plans: each org plan has a fixed
price and optional membership cap, enforced on invite/join
- To charge larger orgs more, tier plans by seat cap (starter 5 /
team 10 / enterprise unlimited), not meter per member
- 'flag_per_seat_prices' does exist in clerk_go but is flag-gated and
not GA, so it's out of scope for this skill
This aligns with /docs/guides/billing/seat-limit-plans.mdx. The
mislabel was the exact framing Dylan flagged in v1 review — worth
being precise about now that the rest is clean.
Updates:
- Description, references table, §6 key pattern, B2B vs B2C table, See Also
- b2b-patterns.md overview + renamed 'Per-Seat Billing' section to
'Seat-Limit Plans' with accurate invariants and add-on requirement note
- Plan Naming table now labels the column 'Seat cap'; mentions
Organization Plans tab + Seat-based toggle
- Eval 3 expected_output + assertions now require the skill to
CORRECT the user's 'auto-incrementing seats' mental model
Payload examples in billing-webhooks.md used 'billing_subscription',
'billing_payer', 'billing_subscription_item', 'billing_payment_attempt'
as the `object` field values. The real wire format uses 'commerce_*'
(verified against @clerk/backend/src/api/resources/JSON.ts and a live
GET /billing/plans returning "object":"commerce_plan").
Any agent writing `if (evt.data.object === 'billing_subscription')`
would never match a real webhook event — silent failure.
Added eval 4 assertion to catch this hallucination going forward.
Three silent failure modes that weren't covered:
- Wrong-tab plan creation. Dashboard Billing → Plans has two tabs
(User Plans / Organization Plans) with slugs scoped per tab, and
plans can't be moved between tabs. Creating `pro` under Organization
Plans and rendering default <PricingTable /> (for="user") produces
an empty table with no error. Quick Start step 2 now calls out the
tab match; both b2c-patterns.md and b2b-patterns.md mirror this.
- Membership required mode silently breaks B2C. If Organizations are
enabled without `Membership optional`, personal accounts are disabled
and <PricingTable /> excludes users with no active org — no console
warning. b2c-patterns.md now opens with this prerequisite + the
dashboard shortcut URL.
- Features are per-plan, not global. /billing/features 404s; features
live inside each plan's edit page, and the same slug can be attached
to multiple plans. Quick Start step 3 clarifies this; eval 6
assertions updated accordingly.
Also added a compact "Dashboard shortcuts" table since every setup
action requires a Dashboard visit and shortcut URLs are more reliable
than click-path instructions.
Agents were writing <PricingTable /> without surfacing the Dashboard
prerequisite, leaving users stuck on the 'cannot_render_billing_disabled'
error with no diagnosis. The Clerk CLI and Backend API cannot toggle
billing today (clerk_go#18260 is the follow-up that will); Dashboard is
the only path.
Changes:
- STOP block at top of SKILL.md calling out Dashboard-only enablement
with the exact deep link
- Quick Start step 1 references the error signature the user will hit
if they skip it
- New 'Error Signatures' section with cannot_render_billing_disabled
plus 5 other common symptoms mapped to root cause + fix
- Version note corrected from 'v6+ (Core 3)' to 'v7+ (Core 3)' —
v6 is Core 2, billing there shipped under __unstable_* APIs
- Eval 1 and 9 assertions tightened: agent must surface the Dashboard
link + error code, and must NOT claim the CLI/BAPI can enable billing
Clerk Billing is a separate product from Stripe Billing — Plans are not
synced to Stripe Products, PricingTable opens Clerk's in-app checkout
drawer (not Stripe Checkout), and per-seat pricing is managed internally
by Clerk with a single active SubscriptionItem per payer per Plan.
Also fixes:
- subscriptionItem.created removed (not a real event; catalog has 9 items)
- useSubscription import: drop __experimental_ prefix (subpath is the signal)
- PricingTable props: full list including for="organization" for B2B
- CheckoutButton section added (was missing entirely)
- Beta warning dropped from SKILL.md per review feedback
- Dev gateway surfaced so skill doesn't falsely require a Stripe account
- Eval assertions rewritten to catch the bad mental model instead of
encoding it (e.g. evals 1, 3, 7, 9, 10, 11)
Code examples were destructuring fields that do not exist at the top
level of billing webhook payloads. Verified against the SDK types in
@clerk/backend (BillingSubscriptionWebhookEventJSON,
BillingSubscriptionItemWebhookEventJSON, BillingPayerJSON).
Webhook payload corrections (SKILL.md Pattern 9, billing-webhooks.md,
b2b-patterns.md):
- The subscribing entity lives at evt.data.payer with user_id? and
organization_id? (not evt.data.user_id / evt.data.org_id)
- The subscription id is evt.data.id (not a separate subscription_id
field)
- Plan slug is nested: evt.data.items[i].plan?.slug on subscription
events, evt.data.plan?.slug on subscriptionItem events
- seats does not exist as a top-level field; derive seat count from
items.length (one subscription item per seat)
- subscriptionItem events carry the item only; no subscription_id
back-reference, so match by payer plus plan slug
- Payload reference now shows the actual nested schema with payer,
items[], items[i].plan, payment_source_id, etc.
useSubscription hook corrections (SKILL.md Pattern 7, b2c-patterns.md):
- Import from @clerk/nextjs/experimental as __experimental_useSubscription
(Billing is Beta, the hook is not yet exported from @clerk/nextjs)
- The return type uses isLoading, not isLoaded
- The renewal date is subscription.nextPayment?.date (a Date object),
not subscription.currentPeriodEnd (which does not exist on
BillingSubscriptionResource)
Event catalog corrections:
- Added subscriptionItem.created back to the setup list (10 items,
not 9). It exists per the SDK types.
- Removed the incorrect "no subscriptionItem.created event" note.
- Added subscriptionItem.created row to the Clerk vs Stripe table.
Also:
- Removed duplicate clerk-setup entry in See Also.
Standardizes the env var declaration across all skills to use the
`compatibility:` string field (matching clerk-webhooks, clerk-testing,
clerk-swift, clerk-android, clerk-expo-patterns) instead of the
nested metadata.inputs list.
Affected:
- clerk-billing (was already part of this PR)
- clerk-orgs
- clerk-backend-api
- clerk-chrome-extension-patterns
- Remove subscriptionItem.created (not a real Clerk event per docs)
- Remove subscription.canceled (cancellation fires as subscriptionItem.canceled)
- Add paymentAttempt.created / paymentAttempt.updated to the setup list
- Add explanatory note clarifying there is no subscription.canceled and
no subscriptionItem.created event
- Relabel the canceled row in the SKILL.md event table to
'Subscription item canceled' to match the actual event semantics
- Destructure seats from subscription.updated so B2B per-seat changes
propagate to the database
Verified against clerk.com/docs/nextjs/guides/development/webhooks/billing.
The SKILL.md Pattern 7 and b2c-patterns.md were teaching agents to read
sessionClaims.metadata.plan directly, which is not the supported path
per Clerk docs. Authorization should go through has({ plan }) and
display-only details through useSubscription().
- Replace sessionClaims lookups with has({ plan }) + useSubscription()
- Update eval 5 to assert the new pattern and explicitly reject
sessionClaims.metadata.plan
- Resolves the contradiction where the skill taught sessionClaims but
evals 2 and 6 penalized it
Replace correctness-focused assertions with discriminating ones that
target Clerk-specific knowledge:
- eval 1 (pricing): check for Beta warning, SDK version pinning,
explicit Stripe-connection prerequisite, Plans-must-exist prereq,
and negative assertions against @clerk/billing import and direct
Stripe API calls.
- eval 4 (webhooks): verify the agent uses verifyWebhook from
@clerk/nextjs/webhooks (not raw svix), uses camelCase event names
like subscriptionItem.pastDue (not past_due), marks the webhook
route public via createRouteMatcher, and uses async middleware
callbacks.
- eval 6 (feature gating): verify the decision-rule explanation
(feature vs plan), the Dashboard → Features slug-matching warning,
and the billing-gates-permissions gotcha.
Measured delta in a local eval run (3 evals, single-run each):
100% pass rate with skill vs 68% without — +32pp differentiation,
up from +17pp with the previous correctness-based assertions.
- Rename `expectations` → `assertions` per https://agentskills.io spec
- Add `files: []` field for consistency with new format
- Keep `scaffold` as a custom extension the eval runner uses to pick
the starter template (nextjs-basic-auth from clerk-nextjs-patterns)
- Tighten individual assertions to be more specific and verifiable:
exact import paths, exact method calls, concrete event names
12 evals, 64 total assertions. Ready for the new skill-creator eval
framework which expects the assertions-based format.
Covers the Clerk Billing feature set:
- `<PricingTable />` component usage
- `has({ plan })` and `has({ feature })` gating (server + client)
- B2B per-seat billing with organizations
- Billing webhook event handling (subscription lifecycle)
- Upgrade/downgrade flows and debugging `has()` failures
- Core 3 syntax throughout (async middleware, auth.protect)
Structure:
- SKILL.md with 10 key patterns and common pitfalls table
- references/ with deep dives for b2c, b2b, components, webhooks
- evals/ with 12 evals covering recommendation + implementation tests
Addresses the measured gap where billing was recommended 6.3% of the
time without a dedicated skill vs 89.6% with one.
Two more residuals caught:
enterprise-sso.md — Custom Sign-In Flow:
- The example used signIn.authenticateWithRedirect() which still works
but is the legacy path. Current enterprise-connections custom flow
doc uses signIn.sso({ strategy, identifier, redirectUrl,
redirectCallbackUrl }). Updated to the Core 3 canonical pattern with
a small Core-2/legacy callout noting the older method still exists
for backwards compat.
- Note: redirectUrlComplete -> redirectUrl, and new redirectCallbackUrl
param for the additional-requirements case.
SKILL.md — §1 accessor note:
- Original line conflated "getAuth(event)" as covering Nuxt AND Astro
server, but Astro uses context.locals.auth() (not getAuth). Split
out per-SDK: auth(event) for Nuxt, context.locals.auth() for Astro,
getAuth(req) for Express. Client accessors kept as useAuth (React)
or composables (Vue/Nuxt).
Two more residuals caught on a fourth cross-validation pass:
SKILL.md §6 (Session Task):
- Claim that TaskChooseOrganization "ships in @clerk/js-frontend" as
an import. Package name is actually @clerk/clerk-js, and the JS
Frontend SDK exposes the task UI via clerk.mountTaskChooseOrganization
/ unmountTaskChooseOrganization methods, not as a component import.
Reworded to make the import-vs-mount distinction clear.
references/invitations.md framework-wrappers table:
- @clerk/backend row pointed at clerk.verifyToken (method-style) but
verifyToken is a standalone top-level export from @clerk/backend,
not a method on the client instance. Reworded accordingly.
Full sweep across all 4 reference files + SKILL.md against docs
reference pages + @clerk/backend + @clerk/astro + @clerk/nuxt
+ @clerk/express source.
Caught in this pass:
SKILL.md:
- Enterprise SSO short section still claimed "Requires domain
verification (DNS TXT)". Reworded to reflect the real model:
the SSO connection owns its domain directly, no separate Verified
Domain required, the two features are mutually exclusive. Added
JIT Provisioning as the real auto-join mechanism.
roles-permissions.md:
- org:sys_domains:manage description claimed coverage of
"Enterprise SSO connections" which isn't in the docs. Tightened
to only what docs explicitly confirm (Verified Domains).
invitations.md:
- createOrganizationInvitationBulk signature was wrong: skill had
a single object with invitations: [...] inside, but the real
method takes organizationId as a positional arg + a params array
as the second arg. Agents following the old example would get a
TypeScript error or a 400 from BAPI.
- Bulk endpoint rate limit corrected from 250/hr (same as single)
to 50/hr (per the bulk-specific rate limit in the backend ref).
- Framework wrappers table had two wrong client retrievers:
@clerk/astro/server used `clerk(context)` (invalid import) —
real is `clerkClient(context)`. @clerk/nuxt/server used
`useClerkClient(event)` — real is `clerkClient(event)`.
Fixed auth-context accessors for both frameworks.
nextjs-patterns.md: verified clean against @clerk/nextjs/server
source (auth.protect overloads, createRouteMatcher, clerkMiddleware).
No changes needed.
Applying feedback from PR review:
enterprise-sso.md:
- Configuration Flow rewritten. Removed DNS TXT record claim for
Verified Domains (ownership is verified via email code inside the
OrganizationSwitcher / OrganizationProfile flow, per the Verified
Domains guide). Removed the conflation of SSO connection setup with
Verified Domains — docs explicitly call out that the two features
are mutually exclusive on the same domain.
- Added note that Dashboard is the only path today; flagged CLI as
a future revisit once shipped.
- Verified Domains section reframed as "separate feature, short
reference" pointing to the canonical doc.
- New section for JIT Provisioning — this is the actual mechanism
that auto-joins SSO users to an org, not Verified Domains.
- Key Rules corrected: SSO connection owns its domain (no Verified
Domain prerequisite); auto-join is JIT Provisioning.
- Dropped self-referential "this is where the skill used to
hallucinate" wording per review.
invitations.md:
- Added framework-wrappers callout at top. Method signatures on
clerk.organizations.* are identical across SDKs — only the
client-retrieval wrapper differs. Table covers @clerk/nextjs,
@clerk/backend, @clerk/astro/server, @clerk/nuxt/server,
@clerk/express. Keeps @clerk/nextjs as default code flavor
(consistent with clerk-billing precedent) while making the
cross-SDK story discoverable, addressing the review comment
on framework separation.
Trim references/nextjs-patterns.md (146 -> 106 lines) by removing
content that already lives in the clerk-nextjs-patterns skill:
- File-naming note (proxy.ts vs middleware.ts) — lives in
clerk-nextjs-patterns/references/middleware-strategies.md
- Generic createRouteMatcher intro — same place
- 401 vs 403 response policy — lives in
clerk-nextjs-patterns/references/api-routes.md
- Generic server action shape — lives in
clerk-nextjs-patterns/references/server-actions.md
Keep the org-specific adaptations:
- auth.protect({ role / permission }) in middleware
- orgSlug === params.slug URL safety invariant
- orgId-scoped DB writes in server actions
- getOrganizationMembershipList usage in an API route
Updated routing + references tables in SKILL.md to describe the
file's scope and point agents to clerk-nextjs-patterns for generic
Next.js patterns.
Brings clerk-orgs in line with the structure used by clerk-billing v2
and clerk-nextjs-patterns: progressive disclosure, compatibility
frontmatter, task-routing table, error signatures, dashboard shortcuts.
Content refinements (cross-referenced against clerk-docs reference
pages + the @clerk/backend + @clerk/shared types in clerk/javascript):
- Permission slugs aligned with the canonical System Permissions
catalog at docs/guides/organizations/control-access/roles-and-permissions.mdx:
org:sys_{memberships,profile,domains,billing}:{read,manage,delete}.
The older table referenced non-canonical names that would not match
a has() check at runtime.
- createOrganizationInvitation example now includes inviterUserId,
which the Backend API reference lists as required for human-initiated
invitations.
- Enterprise SSO example updated to access provider metadata via
enterpriseAccounts[i].enterpriseConnection?.provider, matching the
EnterpriseAccountConnection type definition. Direct .provider access
on EnterpriseAccount returns undefined.
- getOrganizationInvitationList status enum updated to include all
four documented values: pending, accepted, revoked, expired.
- Membership required vs Membership optional mode surfaced explicitly
(default is required since Aug 22, 2025). This affects B2C + B2B
coexistence and was a silent-failure mode worth calling out in the
error signatures table.
Structural changes:
- SKILL.md 423 -> 350 lines. Framework-agnostic framing with
@clerk/nextjs as the default code flavor and a pointer to other SDKs
(@clerk/react, @clerk/astro, @clerk/vue, @clerk/expo, ...). Next.js
middleware and redirect() patterns moved to references/nextjs-patterns.md.
- New references/: roles-permissions.md (120), invitations.md (137),
enterprise-sso.md (108), nextjs-patterns.md (146). All under the 300-line
threshold per skill-creator guidance.
- Frontmatter: inputs: array -> compatibility: string (matches
clerk-billing pattern + skill-creator convention). Version 2.1.0 -> 3.0.0.
- New sections: STOP prerequisite block, Dashboard shortcuts table,
Error Signatures table (seven rows), What Do You Need? routing table.
Evals:
- Eval 2 (RBAC): require canonical System Permission slugs.
- Eval 5 (SSO): require enterpriseConnection?.provider path.
- Eval 7 (invitations): require inviterUserId + full status enum.
- Eval 9 (new): cover the Membership mode prerequisite.