Recipes
This page is the "show me end to end" companion to the rest of the Build section. Each recipe is a complete journey — a real task an editor, rights manager, or product owner actually does — written on two tracks at once. The outer track is the business action, in plain English, the way it happens in a console or a workflow. The inner track, tucked into a collapsible beneath each step, is the exact Public API call that carries it out, so an integrator or an AI agent can reproduce the same journey without guessing at endpoints.
Read the top line of each step and you have the story. Open the
collapsible and you have the request. Everything on the API track is
copy-paste shaped against the real spec: the base is
https://api.packtpub.services, the prefix is /v0/, every request
carries a WorkOS-issued JWT in the
Authorization header, and the Tenant
is read from the token's tenant_id claim — there is no API key and no
tenant header. IDs in the examples use readable slugs
(wk_ml_go, res_ml_go_ch3) so you can follow one thing across the
whole journey.
One rule sits under every recipe and is worth stating once: content must belong to a Work under an active Contract before it can be used in a Product, in distribution, or on any AI surface. The rights that govern content are held once on the Work, projected onto each Resource, and revalidated at the point of use. Most of what these recipes do is, underneath, arranging content so that this one rule resolves the way you want.
flowchart LR
R1["Recipe 1<br/>Manuscript → Product"] --> R2["Recipe 2<br/>Re-sync after edits"]
R1 --> R3["Recipe 3<br/>Compile a course"]
R1 --> R4["Recipe 4<br/>RAG with rights"]
The four recipes build on one another. Recipe 1 gets a manuscript into the estate and sold as a Product. Recipe 2 keeps it current after the author edits the source. Recipe 3 reuses several published books to compile a new course. Recipe 4 puts the finished content behind an AI assistant — with the rights check that every AI surface must make.
These are Build pages, so the code is inline
Everywhere else on the site, JSON lives in collapsibles because the reader is business-facing. Here the collapsibles are a reading aid, not a wall: the API track is meant to be opened, copied, and run. Long payloads are still folded for readability, but nothing is hidden from you.
What is live today vs planned
The Content Lake routes (/v0/content-lake/…) and search are the
whole Public API surface at launch — the Work, Resource,
version, Activity, and rights calls in these recipes run today. The
Product Management routes shown below (/v0/products…) are a
planned addition and are not yet part of the public surface; they
are included so the end-to-end journey reads as one story, and their
request shapes may change before release. The Product lifecycle,
composition, and events they describe are settled — see
Product types and
Product composition.
Recipe 1 — From manuscript to published Product
An editor has a book's chapters as Word documents and wants the book live as a Product. The shape of the journey is: register the intellectual property as a Work, put it under a Contract, ingest the chapters as Resources, snapshot the reviewed versions, compose a Product from them, and publish.
flowchart LR
W["Create Work<br/>wk_ml_go"] --> C["Attach Contract<br/>ctr_chandra_2026"]
C --> I["Ingest chapters<br/>res_ml_go_ch1…"]
I --> S["Snapshot versions<br/>ver_…"]
S --> P["Compose Product<br/>prod_ml_go_book"]
P --> PUB["Publish"]
Step 1 — Register the Work
Create the Work first, so every chapter you upload lands under it — and under its rights — immediately. The Work is the thing the book is, independent of any file.
API — create the Work
POST /v0/content-lake/works HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
Idempotency-Key: wk-ml-go-create-01
A platform.content-lake.work.v1.created event is published on the
platform-content-lake bus. The response carries the new Work id
(wk_ml_go); use it everywhere below. The Idempotency-Key header
makes the create safe to retry.
Step 2 — Put the Work under a Contract
Select or create the Contract that carries all
of the rights and royalty terms, with this Work in its scope. When the
contract goes ACTIVE, the platform resolves the Work's rights and
projects them onto everything under it. Until then the content can be
ingested and worked on, but it is not usable.
API — the contract event that unlocks the Work
Contracts are managed in the Contracts service, which publishes to
the platform-contracts bus. Attaching the Work and activating the
contract emits:
{
"detail-type": "platform.contract.v1.state_changed",
"source": "packt/svc-contracts",
"detail": {
"contract_id": "ctr_chandra_2026",
"work_ids": ["wk_ml_go"],
"from_status": "CONTRACT_STATUS_DRAFT",
"to_status": "CONTRACT_STATUS_ACTIVE"
}
}
The Content Lake consumes this and reprojects the Work's rights onto its Resources — see Works & rights. From this point each Resource's resolved rights answer "what may this be sold as, where, and may AI retrieve it?" without a further call.
Step 3 — Ingest each chapter into the Work
Upload each chapter. The ingestion pipeline archives the file, converts
it to the Packt Content Format
(PCF), splits it, and writes it as a Resource of type DOCUMENT under
the Work — with a Source
record and an INGESTION Activity
attached for provenance.
You can also send PCF directly
The ingestion service is optional. If you already hold structured
content, POST PCF straight to the Resources endpoint under the
Work and skip the connectors entirely. Either way the Resource lands
under the Work and inherits its rights.
API — create a chapter Resource under the Work
POST /v0/content-lake/resources HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
Idempotency-Key: res-ml-go-ch3-create-01
{
"type": "DOCUMENT",
"title": "Chapter 3 — Feature Engineering",
"slug": "ml-go-ch3",
"work_id": "wk_ml_go",
"source_id": "src_ml_go_manuscript"
}
Each write publishes
platform.content-lake.resource.v1.created. Repeat per chapter
(res_ml_go_ch1 … res_ml_go_ch12). The Content Lake then enriches
each Resource automatically — block-level indexing, the
Knowledge Graph entity overlay,
image optimisation — with no further calls from you.
Step 4 — Snapshot the reviewed versions
Editing happens in the source tool; the Content Lake holds explicit
snapshots, not a live copy. The working state is latest. When a chapter
has been reviewed and you want a stable, citable version, take a
snapshot — that is the version a Product will pin to.
API — snapshot a Resource
POST /v0/content-lake/resources/res_ml_go_ch3/versions HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
The response returns a version id (ver_ml_go_ch3_r1) and the
Resource hash (the strong ETag). There is no auto-compaction:
snapshots you create are the versions you can pin. Snapshot every
reviewed chapter before composing the Product.
Step 5 — Compose the Product
In Product Management, create a Product that references the Work. Because rights live on the Work, the Product inherits one coherent answer to what it may be sold as and where. Build the composition — the ordered tree of chapters — pinning each content node to the reviewed version from Step 4.
API — create the Product and its composition
Product Management publishes to the platform-products bus. Create the
shell, then attach the composition:
POST /v0/products HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
{
"type": "Book",
"title": "Machine Learning with Go",
"language": "en",
"work_id": "wk_ml_go",
"concurrent": false,
"composition": [
{ "kind": "FRONT_MATTER" },
{
"kind": "CONTENT",
"resource_id": "res_ml_go_ch3",
"version": "ver_ml_go_ch3_r1"
}
],
"manifestations": [
{
"class": "EBOOK",
"formats": [{ "format": "EPUB", "source": "RENDERED" }]
}
]
}
With concurrent: false, every content node needs an explicit
version, which is what makes a distributable artifact deterministic.
Creating the Product emits platform.products.product.v1.created; as it
enters DRAFT it also emits platform.products.product.v1.drafted.
Step 6 — Publish
Move the Product through its lifecycle — DRAFT → REVIEW → APPROVED →
PUBLISHED. Each transition is a verb; publishing emits
platform.products.product.v1.published, which Distribution and search
indexers consume.
API — advance the lifecycle
POST /v0/products/prod_ml_go_book:transition HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
The six states are DRAFT, REVIEW, APPROVED, PUBLISHED, RETIRED,
WITHDRAWN; entering REVIEW is submitted, then approved, then
published. RETIRED and WITHDRAWN are terminal. The book is now
a Product whose chapters are Resources under one Work, with rights
enforced platform-wide and provenance recorded for every chapter.
Recipe 2 — Re-sync after the author edits the source
The author fixes a broken code sample in chapter 3. Nothing needs to be
re-created: the Content Lake keeps the content identity stable
across the edit, snapshots the new state, and latest-tracking Products
update themselves. This is the errata path — a same-content correction,
recorded as a REVISION, never a new Work.
flowchart LR
E["Author edits<br/>in Word / Google Docs"] --> SN["Connector snapshots<br/>new version"]
SN --> A["REVISION Activity<br/>supersedes prior version"]
A --> EV["resource.v1.updated<br/>change_type: RESOURCE_CHANGE_TYPE_VERSION"]
EV --> L["Latest-tracking<br/>Products re-point"]
EV --> PIN["Pinned Products<br/>notified, not mutated"]
Step 1 — Edit in the native tool
The Content Lake is not the editing surface. The author corrects the chapter in Word Online or Google Docs. The source connector snapshots it on its next pass, producing a new Resource version — the block-level hashing means only the blocks that actually changed are stored and re-processed.
Content identity is preserved
The Resource id (res_ml_go_ch3) does not change across an edit —
only a new ver_ is added. This is why every reference to the
chapter, in every Product, stays valid: the identity is stable, the
version is what moves.
API — read the current version and its hash
Before and after a sync you can compare the Resource hash, which
is the strong ETag. Use hash_content if you only care whether
the content changed (ignoring metadata):
GET /v0/content-lake/resources/res_ml_go_ch3?fields=id,version,hash,hash_content HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
A read defaults to the latest snapshot. A changed hash_content
tells you the correction has landed.
Step 2 — Snapshot the corrected version
Once the connector has pulled the edit into latest, snapshot it so there
is a stable, citable corrected version.
API — snapshot with an errata label
POST /v0/content-lake/resources/res_ml_go_ch3/versions HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
Returns the new version id (ver_ml_go_ch3_r2). A REVISION
Activity records the correction: it pins the broken version in
used, names the corrected one in generated, supersedes the
prior version, and carries subtype: ERRATA so the errata history
is queryable. The prior version is never deleted.
Step 3 — Latest-tracking Products update themselves
The Content Lake emits
platform.content-lake.resource.v1.updated with change_type: RESOURCE_CHANGE_TYPE_VERSION.
Product Management translates that into a product-scoped
platform.products.content.v1.updated and re-points every node tracking
latest. Pinned Products keep producing identical output — that is what
pinning is for.
Subscribe to the product event, not the Content Lake event
Downstream consumers should listen for
platform.products.content.v1.updated on the platform-products bus, not
the Content Lake's Resource event directly. Product Management owns
the Resource-to-Product mapping so you do not have to maintain it.
API — find every Product affected by the edit
To decide what to re-pin, list Products referencing the Resource. This returns both pinned and latest-tracking Products:
GET /v0/products?references_resource=res_ml_go_ch3&page_size=50 HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Paginate with the opaque next_page_token. For pinned Products, you
might notify a human; for latest-tracking Products, the fix has
already taken effect.
Step 4 — Re-pin the ones you want to move
For a Product pinned to the old version, re-pinning is a deliberate editorial action — update the node's version specifier to the corrected version. It never happens automatically.
API — re-pin a content node
PATCH /v0/products/prod_ml_go_book HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
{
"composition": [
{
"kind": "CONTENT",
"resource_id": "res_ml_go_ch3",
"version": "ver_ml_go_ch3_r2"
}
]
}
Re-pinning emits platform.products.content.v1.updated. Downstream
caches and search indexes react normally, and the change lands in
the Product's append-only attribute history.
Recipe 3 — Compile a derived course from several books
A commissioning editor assembles a course from chapters of three
published books. Building a course is a derivation question, never a
manifestation grant — the result is a new Work whose provenance points
back at its sources through USED edges, and whose royalty split is
computable from the ContentUnits it reused.
flowchart TD
B1["Book A chapter<br/>cu_go_conc_ch5"] --> COMP["COMPILATION Activity"]
B2["Book B chapter<br/>cu_k8s_net_ch2"] --> COMP
B3["Book C chapter<br/>cu_ml_go_ch3"] --> COMP
COMP -->|generated| CW["Course Work<br/>wk_go_platform_course"]
COMP -->|USED edges| B1
COMP -->|USED edges| B2
COMP -->|USED edges| B3
Step 1 — Check DERIVE on every source Work
Before any content moves, run a qualified rights check per source: may the platform derive from this Work? A source whose contract prohibits derivation is swapped out here, not discovered later.
API — read the resolved rights on a source Work
Rights are resolved server-side and returned inline on a Work read —
there is no separate check call. Read the Work and inspect the
DERIVE term in its resolved rights view:
GET /v0/content-lake/works/wk_ml_go?fields=id,rights HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
The rights block lists each usage_type with its resolved status,
keyed on the pinned rights_basis:
{
"id": "wk_ml_go",
"rights": {
"usable": true,
"usage_terms": [
{ "usage_type": "DERIVE", "status": "PERMITTED" }
],
"basis": ["ctr_chandra_2026@v3"]
}
}
Branch on the term status (PERMITTED, PROHIBITED,
CONDITIONAL), and on the AIP-193 error reason if the call fails —
never on the message string. Repeat for every source Work before
proceeding.
Step 2 — Excerpt the reused chapters as ContentUnits
Each reused chapter segment is registered as a
ContentUnit (cu_)
via an EXCERPTION Activity — same Work as its source. The ContentUnit
is the unit the royalty split is later computed from.
API — register a ContentUnit by excerption
POST /v0/content-lake/activities HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
{
"type": "EXCERPTION",
"used": [
{ "resource_id": "res_ml_go_ch3", "version": "ver_ml_go_ch3_r2" }
],
"generated": [{ "content_unit_id": "cu_ml_go_ch3" }],
"work_relationship": "SAME_WORK",
"parameters": { "word_count": 4200, "purpose": "course-lesson" }
}
The ContentUnit sits under the source Work, so its resolved rights are the Work's. Repeat per reused segment across all three books.
Step 3 — Compile into a new course Work
A COMPILATION Activity used the ContentUnits and generated the
course Work and its Resource. The USED edge set is exactly what makes
the multi-source royalty split computable.
API — the COMPILATION Activity
POST /v0/content-lake/activities HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
{
"type": "COMPILATION",
"used": [
{ "content_unit_id": "cu_go_conc_ch5" },
{ "content_unit_id": "cu_k8s_net_ch2" },
{ "content_unit_id": "cu_ml_go_ch3" }
],
"generated": [
{ "work_id": "wk_go_platform_course" },
{ "resource_id": "res_go_platform_course_m1" }
],
"work_relationship": "NEW_WORK",
"royalty_flow": "NET_SHARE",
"rights_basis": [
"ctr_chandra_2026@v3",
"ctr_okonkwo_2025@v2",
"ctr_singh_2026@v1"
]
}
royalty_flow: NET_SHARE lets the Royalty service pay each source
author their share by traversing the USED/DERIVED_FROM edges.
rights_basis pins the exact contract versions that authorised each
reuse.
Step 4 — Paper the course Work and build the Course Product
The new course Work is a first-class Work: it needs its own Contract
before anything built on it can ship. Once papered, compose a Course
Product from it — a SCORM_PACKAGE manifestation, an ONLINE
manifestation, or both.
API — create the Course Product
POST /v0/products HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
{
"type": "Course",
"title": "Building Cloud-Native Platforms in Go",
"language": "en",
"work_id": "wk_go_platform_course",
"manifestations": [
{
"class": "SCORM_PACKAGE",
"formats": [
{
"format": "SCORM",
"source": "RENDERED",
"packaging": {
"schema": "SCORM_2004_4E",
"organization": {
"modules_from": "MODULE",
"scos_from": "LESSON"
},
"sequencing": "LINEAR",
"completion": { "rule": "ALL_SCOS", "mastery_score": 0.8 }
}
}
]
}
]
}
Whether the compilation was permitted at all was settled in Step 1; the course Work carrying its own rights basis is what lets the Course Product resolve one coherent answer to what it may be sold as.
Recipe 4 — RAG with rights checking
A developer builds an AI assistant that answers from Content Lake content. The pattern is: search for relevant Resources, read the PCF with its Knowledge Graph entities for context, and — before a single passage reaches the model — confirm the content permits AI retrieval. That last check is not optional: some publishers prohibit AI use, and resolving rights at retrieval time is how the platform respects those terms.
flowchart LR
Q["User question"] --> SR["Search<br/>rights predicate applied"]
SR --> RD["Read PCF + entities<br/>top results"]
RD --> RC["Confirm AI_RETRIEVAL<br/>PERMITTED"]
RC -->|permitted| M["Serve to the model<br/>with attribution"]
RC -->|prohibited| X["Exclude"]
Step 1 — Search with a rights predicate
Run a hybrid search — lexical, semantic, and Knowledge Graph blended —
and add the AI_RETRIEVAL PERMITTED predicate to the same query, so
prohibited content never enters the candidate set. Combine it with tag
filters to scope the search: tag-filtered queries are fast regardless of
result-set size.
Rights filtering is server-side, but assert it anyway
Consequential AI surfaces apply the AI_RETRIEVAL predicate
server-side. Passing it explicitly in your query is belt-and-braces:
it documents intent and keeps your client honest if it is ever
pointed at a non-AI surface.
API — hybrid search, rights-filtered
Search uses sessions
(srch_); the first response returns a search_id to page with.
POST /v0/content-lake/search HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
{
"query": "feature engineering for tabular data in Go",
"search_mode": "hybrid",
"filters": {
"tags": ["topic/machine-learning"],
"rights": { "usage_type": "AI_RETRIEVAL", "status": "PERMITTED" }
},
"page_size": 10
}
The response carries a search_id and ranked Resource ids. Page
with { "search_id": "srch_..." }; previously-returned hits are
excluded automatically.
Step 2 — Read the top results as PCF with entities
For the top-ranked Resources, read the PCF. The abridged read
returns entity ids only, which keeps payloads small — good for fitting
many Resources into a context window. Add ?fields=* (or
?fields=…,entities) to inline the full entity overlay when the model
needs the context. A common pattern: abridged for the initial search,
complete for only the top few.
API — abridged vs complete read
Abridged (entity ids only), streamed as NDJSON for low time-to-first-byte:
GET /v0/content-lake/resources/res_ml_go_ch3 HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Accept: application/x-ndjson
Complete (entities inlined) for the passages you will actually feed the model:
GET /v0/content-lake/resources/res_ml_go_ch3?fields=*,entities HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Accept: application/json
Entities live in an overlay addressed by block _key and
character range, so the same entity can be updated without
re-versioning the Resources that mention it. Render an entity's
name (or a mention's text) as a fallback for any entity type
your code does not recognise.
Step 3 — Confirm AI_RETRIEVAL before serving to the model
The search predicate scopes the candidate set, but the authoritative
check is at the point of use, against the contract versions pinned in
each Resource's rights_basis. Confirm AI_RETRIEVAL is PERMITTED
for each passage before it reaches the model.
API — resolved rights for the exact Resource
GET /v0/content-lake/resources/res_ml_go_ch3?fields=id,rights HTTP/1.1
Host: api.packtpub.services
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
The rights projection includes the resolved term and the basis it
was resolved from:
{
"id": "res_ml_go_ch3",
"rights": {
"usable": true,
"resolved": [
{
"usage_type": "AI_RETRIEVAL",
"status": "PERMITTED",
"rights_basis": ["ctr_chandra_2026@v3"]
}
]
}
}
Serve only passages whose AI_RETRIEVAL is PERMITTED. A
PROHIBITED or CONDITIONAL status excludes the passage — a
CONDITIONAL term must have its condition satisfied before use.
Step 4 — Serve with attribution
Attribution is a first-class concern. When the assistant answers from a passage, surface the source — the author, the title, a link back. The Content Lake provides the provenance; your application is responsible for showing it. Without attribution or click-through, authors lose the reason to publish, which is the whole point the platform protects.
Let the platform reproject; do not cache rights
Contract changes trigger reprojection, so a passage that is permitted today can be prohibited tomorrow if a contract is suspended. Resolve rights at retrieval time rather than caching the decision. Entity metadata can be cached (up to an hour for end-user surfaces, on content ingested more than 24 hours ago), but the rights decision should not be.
Related pages
- Public API — the endpoints, field masks, pagination, and error shape every recipe above builds on.
- Authentication — how the WorkOS JWT and its
tenant_idclaim authorise each call. - Events & webhooks — the events these recipes emit and how to consume them, including CloudEvents at the egress.
- No-code & AI tools — running these journeys from Zapier, n8n, or an LLM agent without writing a service.
- Content Lake — Example flows — the same journeys told business-first, without the API track.
- Explorer — Chapter I — the whole platform, visualised chapter by chapter.