> ## Documentation Index
> Fetch the complete documentation index at: https://developer.jobmojito.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a coaching catalogue

> Organise the coaching portal into directories, place coaching sessions and personas into them with tags, and author custom directory pages.

The **coaching catalogue** is what a learner browses on the coaching portal. It is a tree of **directories**; each directory is one page at `/catalogue/<id>` on your coaching domain, and it can hold two things:

* **Sub-directories** — listed explicitly in the directory's `tags_sub`.
* **Coaching sessions** — *not* listed explicitly. A session appears because its own `tags` match the directory's `tags_interview_set_filter`.

That indirection is the whole design: you tag a session once, and it shows up in every directory whose filter it satisfies. Nothing has to be re-linked when you add a session.

<Note>
  Coaching-platform feature. Directories only render on a coaching portal; they have no effect on the recruiting/interview portal.
</Note>

## The mental model

```text theme={null}
merchant.catalogue_tag_start          →  the directory the catalogue opens on
└── home-employee-en                     (is_start_directory: true)
    ├── tags_sub: [sales-coaching-en, interview-prep-en]
    │   ├── sales-coaching-en
    │   │   ├── tags_interview_set_filter: ["sales"]
    │   │   └── lists every active coaching/persona session tagged "sales"
    │   └── interview-prep-en
    └── content_md: optional custom page that lays the above out by hand
```

| Tool                                                        | What it is for                                                                                          |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `list_catalogue_directories` (`GET /catalogue-tag-list`)    | Find directories; walk the tree with `parent_tag`; spot the root with `is_start_directory`.             |
| `get_catalogue_directory` (`GET /catalogue-tag-get`)        | One directory in full — its `content_md`, its sub-directories, and **the sessions it currently lists**. |
| `create_catalogue_directory` (`POST /catalogue-tag-create`) | Create a directory, optionally nested under a `parent_tag`.                                             |
| `update_catalogue_directory` (`POST /catalogue-tag-update`) | Change a directory, its filter, its children, or its custom page.                                       |
| `update_interview` (`POST /job-interview-update`)           | Set `tags` on an existing session so it lands in a directory.                                           |

## How a session gets into a directory

A session is listed when **all** of these are true:

| Condition                                                                       | Where it comes from                                                        |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Its `tags` contain **every** tag in the directory's `tags_interview_set_filter` | `tags` on the session, `tags_interview_set_filter` on the directory        |
| Its `type` is `coaching` or `persona`                                           | `type` on create; `persona-create` with `portal: "coaching"` (the default) |
| Its `status` is `active`                                                        | `status` on create, or `set_interview_state`                               |
| Its `visibility` is `public` or `merchant_public`                               | `visibility` on create/update                                              |

<Warning>
  The tag filter is an **AND, not an OR**. A directory filtering on `["sales", "objection-handling"]` lists only sessions carrying *both* tags. A session may carry extra tags — that is what lets one session appear in several directories.
</Warning>

Two edge cases are worth knowing:

* `tags_interview_set_filter: []` (empty array) matches **every** tagged session — a useful "everything" directory, and an easy accident.
* `tags_interview_set_filter: null` matches **nothing**. A brand-new directory created without the field lists no sessions until you set it.

## Walkthrough

<Steps>
  <Step title="See what already exists">
    Start from the root and walk down, rather than guessing ids.

    ```bash theme={null}
    # Everything you can see, newest ids first — is_start_directory marks the root
    curl "https://cool.jobmojito.com/functions/v1/catalogue-tag-list?mojito_language_code=en" \
      -H "Authorization: Bearer $SUPABASE_JWT"

    # Just the children of one directory, in the order they are rendered
    curl "https://cool.jobmojito.com/functions/v1/catalogue-tag-list?parent_tag=home-employee-en" \
      -H "Authorization: Bearer $SUPABASE_JWT"
    ```

    <Tip>
      `include_public=true` (the default) also returns the platform-wide directories shared across merchants. Set it to `false` to see only your own.
    </Tip>
  </Step>

  <Step title="Create the directory">
    The `id` is the URL segment and **cannot be changed later** — other directories reference it in their `tags_sub`. Use lowercase letters, digits and single `-`/`_` separators, and end it with the language code by convention.

    ```bash theme={null}
    curl -X POST https://cool.jobmojito.com/functions/v1/catalogue-tag-create \
      -H "Authorization: Bearer $SUPABASE_JWT" \
      -H "Content-Type: application/json" \
      -d '{
        "id": "sales-coaching-en",
        "name": "Sales coaching",
        "description": "Practice discovery, objection handling and closing.",
        "mojito_language_code": "en",
        "visibility": "merchant_public",
        "status": "active",
        "parent_tag": "home-employee-en",
        "tags_interview_set_filter": ["sales"]
      }'
    ```

    `parent_tag` appends the new id to that directory's `tags_sub`, so the page is reachable immediately. Omit it and the directory exists but is only reachable by direct link until you add it to a parent.

    The response returns `catalogue_url` — the live page — when your merchant has a coaching domain configured.
  </Step>

  <Step title="Tag the sessions">
    A **new** session takes its tags on create. All three creation endpoints accept `tags`:

    <CodeGroup>
      ```bash Coaching session (AI-generated questions) theme={null}
      curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-create \
        -H "Authorization: Bearer $SUPABASE_JWT" \
        -H "Content-Type: application/json" \
        -d '{
          "name": "Handling price objections",
          "location": "remote",
          "interview_template_id": "TEMPLATE_UUID",
          "mojito_language_code": "en",
          "status": "active",
          "type": "coaching",
          "visibility": "merchant_public",
          "tags": ["sales", "objection-handling"]
        }'
      ```

      ```bash Role-play persona theme={null}
      curl -X POST https://cool.jobmojito.com/functions/v1/persona-create \
        -H "Authorization: Bearer $SUPABASE_JWT" \
        -H "Content-Type: application/json" \
        -d '{
          "name": "Objection-handling role-play",
          "interview_template_id": "TEMPLATE_UUID",
          "mojito_language_code": "en",
          "status": "active",
          "visibility": "merchant_public",
          "portal": "coaching",
          "persona_role_avatar": "is to act as a skeptical procurement manager",
          "persona_role_user": "is to be a sales rep booking a follow-up demo",
          "tags": ["sales", "objection-handling"]
        }'
      ```
    </CodeGroup>

    An **existing** session is re-tagged with `update_interview`. `tags` is replaced wholesale, so read the current value first and send the extended list:

    ```bash theme={null}
    # 1. read the current tags
    curl "https://cool.jobmojito.com/functions/v1/job-interview-get?position_id=SESSION_UUID" \
      -H "Authorization: Bearer $SUPABASE_JWT"

    # 2. send the full list, not just the addition
    curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-update \
      -H "Authorization: Bearer $SUPABASE_JWT" \
      -H "Content-Type: application/json" \
      -d '{ "position_id": "SESSION_UUID", "tags": ["sales", "objection-handling"] }'
    ```

    <Note>
      `persona-create` defaults to `portal: "coaching"`, which is the variant that belongs in a catalogue. A persona created with `portal: "interview"` is stored as `persona_interview` and is **never** listed in the catalogue — it goes through the recruiting invitation flow instead. See [Create an interview](/cookbooks/create-an-interview#two-variants).
    </Note>
  </Step>

  <Step title="Verify the mapping">
    Do not assume the tags matched. `get_catalogue_directory` returns the sessions the directory actually lists, applying exactly the rules the live portal applies:

    ```bash theme={null}
    curl "https://cool.jobmojito.com/functions/v1/catalogue-tag-get?id=sales-coaching-en" \
      -H "Authorization: Bearer $SUPABASE_JWT"
    ```

    ```json theme={null}
    {
      "id": "sales-coaching-en",
      "tags_interview_set_filter": ["sales"],
      "matched_sessions": [
        { "id": "9c1b…e4f2", "name": "Handling price objections", "type": "coaching",
          "status": "active", "visibility": "merchant_public",
          "tags": ["sales", "objection-handling"] }
      ],
      "sub_directories": [],
      "catalogue_url": "https://coaching.example.com/catalogue/sales-coaching-en"
    }
    ```

    An empty `matched_sessions` almost always means one of four things: the session is still `draft`, its visibility is `merchant_invite`/`merchant_unlisted`, its `type` is not `coaching`/`persona`, or its tags are missing one of the filter's tags.
  </Step>

  <Step title="Author a custom page (optional)">
    By default a directory renders as a plain grid: sub-directories, then sessions. Set `content_md` and your Markdown takes over the layout instead.

    ```bash theme={null}
    curl -X POST https://cool.jobmojito.com/functions/v1/catalogue-tag-update \
      -H "Authorization: Bearer $SUPABASE_JWT" \
      -H "Content-Type: application/json" \
      -d '{
        "id": "sales-coaching-en",
        "content_md": "## Sales coaching\n\nStart with discovery, then work up to objections.\n\n[plan-progress]\n\n### Practise a conversation\n\n[sessions:filter=objection-handling,limit=6]\n\n### Go deeper\n\n[directory:sales-coaching-closing-en]\n"
      }'
    ```

    Directives must sit **alone on their own line** — one on a line with other text renders as ordinary text:

    | Directive                            | Renders                               |
    | ------------------------------------ | ------------------------------------- |
    | `[plan-progress]`                    | The learner's coaching-plan progress. |
    | `[directory:<tag-id>]`               | A card for one sub-directory.         |
    | `[session:<interview-id>]`           | A card for one session.               |
    | `[sessions]`                         | Every session in this directory.      |
    | `[sessions:<term>]`                  | Sessions matching a term.             |
    | `[sessions:filter=<term>,limit=<n>]` | A filtered, capped list.              |

    <Warning>
      `content_md` is replaced wholesale. Read the current page with `get_catalogue_directory` and send the edited whole — not a fragment. Sending `null` removes the custom page and restores the default grid.
    </Warning>

    <Tip>
      A directive can only show sessions the directory already lists. If `[sessions:filter=…]` renders nothing, check `matched_sessions` first — the filter narrows that set, it does not widen it.
    </Tip>
  </Step>
</Steps>

## One directory per language

The catalogue groups directories by `mojito_language_code`, and the portal's directory switcher only offers directories in the visitor's language. A bilingual catalogue is therefore two parallel trees — `sales-coaching-en` and `sales-coaching-de` — each nested under its own language's root, each with its own sessions. Tags themselves are language-neutral, so the same tag can serve both trees if the sessions are language-specific.

## Coaching plans

`coach_plan` marks a directory (or a session) as a stage of a guided plan — `demo`, `screening`, `2nd`, `3rd`, `closing`, `job-specific`, `other`. Learners then see their progress through those stages wherever a page carries the `[plan-progress]` directive. Leave it null for anything that is not part of a plan.

## Gotchas

| Gotcha                                                                                          | What to do                                                                                                                        |
| ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Tag filter is an AND                                                                            | Every tag in `tags_interview_set_filter` must be on the session. Filter on one tag unless you deliberately want the intersection. |
| `tags`, `tags_sub`, `tags_interview_set_filter` and `content_md` are all **replace, not merge** | Read the current value first, then send the full new value.                                                                       |
| An empty `tags_interview_set_filter` matches everything tagged                                  | Set the filter deliberately; `[]` is not "nothing".                                                                               |
| The directory `id` is permanent                                                                 | It is the URL and other directories reference it. To rename the *page*, change `name`; to change the URL, create a new directory. |
| A session in `draft` never appears                                                              | Publish it with `set_interview_state` (or `status: "active"` on `update_interview`).                                              |
| `merchant_invite` / `merchant_unlisted` sessions never appear                                   | The catalogue only lists `public` and `merchant_public`.                                                                          |
| An id in `tags_sub` that no longer resolves                                                     | `get_catalogue_directory` returns it in `tags_sub` but not in `sub_directories` — compare the two to find broken links.           |
| A session's `type_credit` decides which consumer credit bucket a learner spends                 | Set it on `update_interview` if the default (`interview_coach_manager`) is wrong for the session.                                 |

## Next steps

<CardGroup cols={2}>
  <Card title="Create an interview" icon="plus" href="/cookbooks/create-an-interview">
    Create the coaching sessions and role-play personas the catalogue lists.
  </Card>

  <Card title="Review results" icon="clipboard-check" href="/cookbooks/review-results">
    Read transcripts and feedback once learners complete a session.
  </Card>
</CardGroup>
