Skip to content

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
{
  "title": "Machine Learning with Go",
  "slug": "ml-go"
}

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_ch1res_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
{
  "label": "Reviewed for first edition"
}

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
{ "action": "submitted" }

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
{
  "label": "Errata — corrected chapter 3 code samples"
}

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.

  • Public API — the endpoints, field masks, pagination, and error shape every recipe above builds on.
  • Authentication — how the WorkOS JWT and its tenant_id claim 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.