Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
65f2014
fix(skills): correct pricing examples, price list resolution and prom…
Plopix Oct 30, 2026
19bb60d
fix(skills): make the query examples match the real schemas
Plopix Oct 30, 2026
e49e848
fix(skills): make the payments helpers work and stop payment writes u…
Plopix Oct 30, 2026
5bc38ad
fix(skills): correct mutation calls and cart, order and publish behav…
Plopix Oct 30, 2026
3066112
fix(skills): correct content-model defaults, limits and component config
Plopix Oct 30, 2026
33bbd06
fix(skills): correct mass-operations graphql, permissions and silent …
Plopix Oct 30, 2026
72a66dd
fix(skills): say the MCP mass-operation tools refuse price tiers
Plopix Oct 30, 2026
37c9bb7
fix(skills): correct vector-ranking window, indexing and invalid exam…
Plopix Oct 30, 2026
12775a4
fix(skills): remove the nonexistent topic component and filter from t…
Plopix Oct 30, 2026
7ab0995
fix(skills): align mutation, query and content-model with vector rank…
Plopix Oct 30, 2026
c720fe0
fix(skills): correct client auth, shop pipeline ids and helper examples
Plopix Oct 30, 2026
1f90f8b
fix(skills): correct stale booking race, availability and capacity cl…
Plopix Oct 30, 2026
5c347c9
fix(skills): say shopBookingApi cannot reach the booking admin API
Plopix Oct 30, 2026
874df76
fix(skills): correct subscription renewals, shop sync and stubbed end…
Plopix Oct 30, 2026
15103a4
fix(skills): correct agent-tenants rate limit and add capacity, invit…
Plopix Oct 30, 2026
e6a7e85
fix(skills): correct plugin placements, install flow, payload claims …
Plopix Oct 30, 2026
7355539
fix(skills): say bearer tokens reach PIM as well as Core
Plopix Oct 30, 2026
f6d9aa5
fix(skills): correct the permissions role api, token model and field-…
Plopix Oct 30, 2026
7daeeff
fix(skills): correct content-json text rules and image nodes
Plopix Oct 30, 2026
2e77064
fix(skills): stop data-creation claiming bulk imports and map its pay…
Plopix Oct 30, 2026
bb0c9c7
fix(skills): warn that variant price and stock keys are dropped
Plopix Oct 30, 2026
3a8c399
fix(skills): correct responsive image formats, focal point and Image …
Plopix Oct 30, 2026
1d38dc1
fix(skills): correct folder operations and path behaviour in informat…
Plopix Oct 30, 2026
b8c8cc6
fix(skills): say Core stage moves do not reach the Shop store
Plopix Oct 30, 2026
b650f9e
fix(skills): keep the subscriptions description within the limit
Plopix Oct 30, 2026
d1a4c73
chore(skills): add reference selection cases for the new references
Plopix Oct 30, 2026
887f020
fix(skills): make syncStages and ensureCustomer fail loudly instead o…
Plopix Oct 30, 2026
4e0075d
fix(skills): warn that the shape examples empty anything that already…
Plopix Oct 30, 2026
54dd915
fix(skills): warn about meta replacement and double charging in subsc…
Plopix Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,30 @@
"task": "boost high-margin products in category listings",
"skills": ["vector-ranking"],
"expected": ["vector-ranking/ranking-signals"]
},
{
"task": "write the shared helper that creates the order once and records a payment for any provider",
"skills": ["payments"],
"expected": ["payments/crystallize-payments-lib"]
},
{
"task": "build a back-office tool that takes a bookable unit out of service",
"skills": ["bookable-resources"],
"expected": ["bookable-resources/booking-admin"]
},
{
"task": "merge several near-identical product shapes into one",
"skills": ["content-model"],
"expected": ["content-model/shape-consolidation"]
},
{
"task": "show me a complete operations file that creates a content model in the right order",
"skills": ["content-model"],
"expected": ["content-model/mass-operation-example"]
},
{
"task": "create a custom role with the API and assign it to a user",
"skills": ["permissions"],
"expected": ["permissions/role-api"]
}
]
30 changes: 17 additions & 13 deletions use-crystallize/skills/agent-tenants/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ curl -s -X POST https://api.crystallize.com/agents/tenants \
| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name` | yes | String, 1 to 100 characters once trimmed. The tenant's display name. |
| `identifier` | no | String, 1 to 100 characters. Lowercased; anything outside `a-z 0-9 -` becomes `-`. Without it (or if nothing usable remains) it comes from `name`, else `agent-<8 hex>`. |
| `source` | no | String, 1 to 100 characters. Who is provisioning (`claude-code`, `cursor`). Shown in Crystallize's Slack and as the agent user's name. |
| `source` | no | String, 1 to 100 characters. Who is provisioning (`claude-code`, `cursor`). Shown as the agent user's name. |

Any other key is ignored, `omitDefaultShapes` included. Values that are not plain strings are refused with 400.

Expand Down Expand Up @@ -168,12 +168,14 @@ EOF

- **The claim invite's role must be `tenantAdmin`**, for whoever will own the tenant: the user, or someone they
name. Only a human `tenantAdmin` claims the tenant. An invite with any other role lets a human in and keeps the
tenant from deletion, but never claims it: send one on top of the admin invite, never instead of it.
tenant from deletion, but does not claim it: send one on top of the admin invite, never instead of it.
- **`expiresAt` is optional in the schema; send it anyway**, no later than the tenant's. An invite that
outlives the tenant is useless.
outlives the tenant is useless. A date in the past is accepted and gives a dead link: if a human has kept the
tenant past its deadline, send a future date.
- **Errors come back in the result, not as HTTP errors.** An unknown role gives `RoleNotFoundError` instead of
`InviteToken`.
- You can create a new invite at any time, for instance when one has expired.
- **An invite works once.** A second human needs a second invite, and a used link only shows an "already
redeemed" error. You can create a new invite at any time, for instance when one has expired or been used.

## 4 · Hand the invite to the human

Expand All @@ -198,9 +200,8 @@ What happens next needs no call from you:

1. The App redeems the token (`redeemInviteToken`) and the human becomes `tenantAdmin` on the tenant.
2. At the next cleanup run, within 2 hours, Crystallize sees a human `tenantAdmin` and the tenant is claimed:
its provisioning record is deleted, so it can never be removed automatically.
3. Crystallize's Slack gets a "claimed" message, and the human is subscribed to the newsletter under the usual
signup rules.
from then on it can never be removed automatically.
3. The human is subscribed to the newsletter under the usual signup rules.

## 5 · Check whether the human joined (optional)

Expand Down Expand Up @@ -236,17 +237,18 @@ they join, a second member appears with `role: "tenantAdmin"` and `kind: "human"
| Topic | Rule |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Lifetime | `expiresAt` = provisioning + 72 h. A cleanup runs every 2 hours, so an unclaimed tenant goes between 72 h and about 74 h later, with all its content, the agent user and its token. |
| What keeps a tenant | Any human with a role on it. A human `tenantAdmin` claims it for good. A human with a lesser role keeps it from deletion but never claims it: it waits for a human admin. |
| What keeps a tenant | Any human with a role on it. A human `tenantAdmin` claims it for good. A human with a lesser role keeps it from deletion but does not claim it: it waits for a human admin. |
| Promoting a human | Promoting a lesser-role human to `tenantAdmin` claims the tenant: `setUserRole(userId: "<id from step 5>", role: "tenantAdmin")` on the Core API, or the user list in the App. |
| Nothing else | No call extends the 72 hours. Only a human joining keeps the tenant. |
| After the claim | The agent stays `tenantAdmin` and its token keeps working. The human can remove the agent user in the App; the cleanup then deletes it and its tokens. |
| One tenant per call | An agent cannot create tenants any other way (`createTenant` on `/@me`, PIM `tenant.create`), copy a tenant, or redeem invites. Another tenant is another `POST /agents/tenants`. |
| The agent user | Named `Agent <source>`, no email, and no one can log in as it. It acts only through its token, which works on its own tenant only. |
| Rate limit | None specific to provisioning. The API firewall's per-IP limit applies to every call (429). |
| Rate limit | Provisioning has its own limit, per IP and across all callers, on top of the per-IP limit on every call (429). On a shared cloud IP you can hit it with few calls of your own. |

## Errors

Provisioning answers plain HTTP errors with a JSON body, `{ "message": "…", "statusCode": 400 }`. GraphQL calls
answer HTTP 200 with the error inside the result union (`__typename`, `errorName`, `message`).
Provisioning answers plain HTTP errors with a JSON body, `{ "message": "…", "statusCode": 400 }`. Authenticated
GraphQL calls answer HTTP 200 with the error inside the result union (`__typename`, `errorName`, `message`).

| Call | Status | Message | What to do |
| ---------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
Expand All @@ -256,8 +258,10 @@ answer HTTP 200 with the error inside the result union (`__typename`, `errorName
| Provision | 400 | `source must be a string of 1 to 100 characters.` | Fix or drop `source`. |
| Provision | 409 | `That tenant identifier was just taken. Please retry.` | Retry the same call once: a new identifier is picked. Nothing was created. |
| Provision | 500 | `Provisioning failed (trace <id>).` | Retry later; give the trace id to Crystallize support if it repeats. Half-created is cleaned. |
| Provision | 503 | `Crystallize cannot provision more agent tenants right now. …` | At capacity. Retry later, or have the user create a tenant and give you an access token. |
| Provision | 404 | none | The endpoint is not deployed on that host. Tell the user. |
| Any | 429 | none | Too many requests from this IP: back off and retry. |
| Any | 429 | none | Too many requests. Provisioning is also limited across all callers: wait minutes, then retry. |
| Any call, once the tenant is deleted | 403 | `Invalid access token pair provided` | The tenant, its agent user and its token are gone. Provision again and replay your build. |
| `createInviteToken` | 200 | `RoleNotFoundError`: `Role not found: <role> (tenant id <id>)` | The claim invite takes `tenantAdmin`; any other role must exist on the tenant. |
| Agent-forbidden: `/@me` `createTenant`, invite redemption, tenant copy | 200 | `UnauthorizedError`: `This operation is not available to agent users.` | Do not retry. Create tenants with step 1 only, and let humans redeem invites. |

Expand All @@ -270,7 +274,7 @@ answer HTTP 200 with the error inside the result union (`__typename`, `errorName
| Calling `/@<the identifier you sent>` | The calls fail when the identifier was taken and got a suffix | Use the returned `identifier` and `apiUrl` |
| Provisioning again to start over while the tenant is alive | A second tenant, agent user and token; the first lingers for 72 h | Fix or recreate the shapes in the tenant you have |
| Building only with one-off mutations | An expired tenant takes the whole build with it | Keep the build as a mass operations file and replay it into a new tenant |
| Sending only an invite with another role | The human gets in and keeps the tenant, but it is never claimed | Always create a `tenantAdmin` invite for whoever will own it; others go on top |
| Sending only an invite with another role | The human gets in and keeps the tenant, but it is not claimed | Always create a `tenantAdmin` invite for whoever will own it; others go on top |
| An invite with no `expiresAt`, or one past the tenant's | A link that still looks valid after the tenant is gone | Pass the tenant's `expiresAt` |
| Giving the link without the deadline | The user opens it on day four and finds nothing | Always state the date and time with the link |
| Saying "it's yours, nothing to transfer" after provisioning | The user never opens the invite, and the tenant is deleted | The tenant is the agent's until a human `tenantAdmin` joins |
Expand Down
29 changes: 19 additions & 10 deletions use-crystallize/skills/bookable-resources/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ name: bookable-resources
description: >
Sell Crystallize products that are held in time rather than counted in stock — rentals, services,
rooms, courses, equipment, appointments. Covers booking policies and bookable pools on the Core API,
and the booking flow on the Shop API /cart endpoint: availability, holds, checkout and confirmation.
the booking flow on the Shop API /cart endpoint (availability, holds, checkout and confirmation), and
the Shop API /booking/admin endpoint (maintenance blocks, reservation calendars, staff cancellations).
Use when the user wants to rent something out, take bookings or reservations, sell time slots or
by the day/weekend/week, show a booking calendar, manage a fleet of machines, rooms or seats, or
handle cancellations. Trigger on "bookable", "booking", "reservation", "rental", "rent", "hire",
"availability", "time slot", "calendar", "hold", "setBookable", "bookSkuItem",
"createBookingPolicy", "confirmCartBooking", "cancelReservation", "rebookReservation",
"nearestAvailability", "checkBooking", "booking policy", "cancellation window", "pool", "units".
by the day/weekend/week, show a booking calendar, manage a fleet of machines, rooms or seats, take a
unit out of service, or handle cancellations. Trigger on "bookable", "booking", "reservation",
"rental", "rent", "hire", "availability", "time slot", "calendar", "hold", "setBookable",
"bookSkuItem", "createBookingPolicy", "confirmCartBooking", "cancelReservation", "rebookReservation",
"nearestAvailability", "checkBooking", "booking/admin", "booking policy", "cancellation window",
"pool", "units".
metadata:
author: Crystallize
version: "1.0"
Expand Down Expand Up @@ -80,26 +82,31 @@ for the policies to find out whether this session can manage them:

A connection means yes. `FORBIDDEN` means the role lacks the `bookingPolicies` permission. That
usually happens on a custom role created before bookings existed, and it is fixed on the role, not in
the query.
the query. Putting a pool on a product is a second permission: `setBookable`, `reapplyBookablePolicy`
and `clearBookable` need `productBookable` on top of the right to edit the item.

## Failure modes

Three of these are silent — a paid booking whose reservation has no order, a pool or policy change that
has no effect, and a booking that vanishes when two are taken at once. They are the reason this skill
exists; the rest name themselves.
has no effect, and a bookable SKU sold with nothing held. They are the reason this skill exists; the
rest name themselves.

| Symptom | Cause | Fix |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| Admin shows "No order — not checked out" on a paid booking | `confirmCartBooking` ran before the cart had its `orderId` | Confirm **again** after `createFromCart` — see [booking-flow](references/booking-flow.md) |
| An order for a bookable product with nothing reserved | The SKU went through `addSkuItem` or `hydrate` with no `booking`: a plain line | Book with `bookSkuItem`, or send `booking` on the `hydrate` line |
| `NotBookable` on a product you just made bookable | The item is not published, or the query's `language` is wrong | Publish it; pass the language the item exists in |
| A pool or policy change has no effect in the Shop | Bookable edits are drafts; products keep the `policySnapshot` they were set with | `reapplyBookablePolicy` for a policy change, then **publish** the product |
| `FORBIDDEN` from `createBookingPolicy` | The role has no `bookingPolicies` permission | Grant it on the role |
| Holds vanish within seconds, or `InvalidRange` on every date | A duration was sent in minutes, hours or days | Every policy duration is **seconds**; read `humanized` back to check |
| `hydrate` throws "A placed cart cannot be hydrated" | The cart is placed; its contents are frozen | Change bookings before `place`, or through `/booking/admin` after |
| `ReservationConflict` on a unit that looked free | Someone took it between the availability query and the booking | Walk the other `freeUnitIds` and retry |
| Two bookings taken at once, one of them missing | Concurrent `bookSkuItem` on one cart — both answered `Cart` | Serialize booking writes per cart, then read the cart back |
| `ReservationConflict` whose `message` says the cart changed | Another write landed on the same cart first, and nothing was booked | Read the cart back, then retry; serialize booking writes per cart |
| `ReservationConflict` on a capacity pool with seats free | Capacity counts every booking overlapping the window, not the busiest moment | Keep capacity windows uniform, or use units |
| `Cannot place this cart: <sku> (<reason>)` | `place` or `hydrate` could not keep a booking line; see the reason | Nothing was saved: `hydrate` with another window or without the line |
| `InvalidRange` on a date far ahead | The window is past the policy's `advanceWindow` | Widen the policy, or cap the calendar at the shortest `advanceWindow` in the basket |
| `CancellationWindowClosed` when removing a basket line | The window applies to holds that were never bought | Re-hydrate the cart without that line instead |
| `Unexpected error.` from `addCartItem` or `removeCartItem` | They refuse a booking line | `hydrate` with the new quantity, or without the line |
| `BookablePoolKindChangeError` | Capacity → units while a capacity pool is published | `clearBookable`, publish, cancel or wait out open reservations, then set units |
| `BookingPolicyInUseError` on delete | Products still reference the policy (`stats.referencingProductCount`) | Move those products to another policy and publish them |

Expand All @@ -109,6 +116,8 @@ exists; the rest name themselves.
capacity pools, snapshots and reapplying, deleting and clearing.
- [references/booking-flow.md](references/booking-flow.md) — Shop API `/cart`: availability, holds,
checkout, the double confirmation, cancelling and moving a booking.
- [references/booking-admin.md](references/booking-admin.md) — Shop API `/booking/admin`: maintenance
blocks, the reservation calendar and list, moving a reservation to another unit.
- [references/modelling.md](references/modelling.md) — periods as variants, units as real machines,
what Discovery serves, and how a rental relates to the product it is a rental of.

Expand Down
Loading
Loading