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

# Search prompts for AI assistants

> Copy-paste prompts for your AI coding assistant: add job search to your app, or audit an existing search integration for the API's silent failure modes.

These prompts hand your AI coding assistant the complete search contract — including the parts that fail *silently*, which are exactly the parts an assistant otherwise gets wrong while everything still returns `200`. Each prompt is self-contained.

<Note>
  These prompts build against **raw HTTPS**, so the result carries no third-party dependency and the assistant works from the contract on this page. That is a preference, not a requirement — the [official clients](/docs/sdks) cover this whole surface as of 4.0.0, including `search_description`, the date filters, and `include_fields`. If you would rather use one, the contract below still reads as the reference for what the endpoints do.
</Note>

## Pick your prompt

| Your situation                                                | Use                                                                                                                      |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| No search yet — wire job search into our app                  | [Add job search](#add-job-search)                                                                                        |
| We already call the search API — check it for silent failures | [Audit an existing integration](#audit-an-existing-integration)                                                          |
| We're on a client library older than 4.0.0                    | Use the [upgrade prompt](/docs/guides/feed-integration-prompts#upgrade-the-client-library) — it covers the search changes too |

<Steps>
  <Step title="Open your assistant at the root of your repo">
    Claude Code, Cursor, Copilot — anything that can read your codebase and edit files.
  </Step>

  <Step title="Copy one prompt below and paste it as your message">
    The copy button on the code block grabs the whole prompt.
  </Step>

  <Step title="Answer its questions, then review the diff">
    Both prompts make the assistant inspect your stack and report back before writing code.
  </Step>
</Steps>

***

## Add job search

For an app with **no Jobo search yet**. Produces a search service, a route, and (if your app has a frontend) a results view with filters — in your existing stack.

````markdown Add-job-search prompt [expandable] theme={null}
# Task: add Jobo job search to this application

Wire the Jobo Enterprise search API into this app: a search service, a thin
route/endpoint of our own, and — if this repo has a frontend — a results view
with filters and pagination. Everything over raw HTTPS — no vendor SDK.

## First: inspect this repo and confirm the plan

Before writing any code, examine the codebase and report back:
- Language(s), framework, and (if present) the frontend stack
- The HTTP client already in use
- Any caching layer (Redis, in-memory, HTTP cache)
- How config and secrets are handled

Propose where the new code will live, following this repo's conventions, and
wait for my confirmation. Use the existing stack; ask me before adding any
dependency.

## Hard rules

1. Raw HTTP. Build this against the endpoints below rather than installing a
   client library — we want no third-party dependency in this path. (Official
   clients exist and cover this surface at 4.0.0; we are choosing not to add
   one here.)
2. Read the API key from the `JOBO_API_KEY` environment variable (or this
   repo's secret mechanism) — server-side only. Never expose the key to a
   browser; our own route proxies the search.
3. Use only the endpoints, parameters, and values in this prompt. Do not invent
   parameters: the API silently ignores unknown parameters and silently
   returns zero results for unknown filter values, so a guess will look like
   it worked while filtering nothing.

## API basics

- Base URL: `https://connect.jobo.world`. HTTPS only.
- Auth: `X-Api-Key: <key>` header. No `Authorization: Bearer` form, no
  query-parameter auth. All JSON is snake_case in both directions.
- `Content-Type: application/json` on POST.

## The two search endpoints (same index, same response shape)

### GET /api/jobs — query-string search

Use for simple queries and anything URL-shareable. Parameters:

- `q` — free-text query. Matches title, company, popular skills, and summary
  with typo tolerance. Wrap in double quotes (`q="Quantitative Developer"`)
  for an exact contiguous phrase match against the title only.
- `search_description` — boolean, default `true`. Set `false` to restrict
  matching to titles and curated alternative titles: faster, more precise.
- `location` — ONE location string (city, region, or country).
- `sources`, `work_model`, `employment_type`, `experience_level`, `skills`,
  `industries` — comma-separated lists.
- `posted_after`, `posted_before` — ISO 8601; employer posting date.
- `discovered_after`, `discovered_before` — ISO 8601; first-indexed time.
- `min_salary_usd`, `max_salary_usd` — integers, annual USD.
- `page` — 1-indexed, default 1.
- `page_size` — default 25, range 1–100. Out of range IS a real `400`.
- `include_fields`, `include_facets` — see field selection below.

### POST /api/jobs/search — typed JSON body

Use for filter UIs, multiple queries, and include/exclude logic. Body fields:

```json
{
  "queries": ["backend engineer", "\"Platform Engineer\"", "-crypto"],
  "locations": ["Berlin", "Remote"],
  "sources": ["greenhouse", "lever"],
  "work_models": ["remote", "hybrid"],
  "employment_types": ["full-time", "contract"],
  "experience_levels": ["senior", "lead"],
  "skills": { "include": ["python"], "exclude": ["php"] },
  "industries": { "include": ["fintech"], "exclude": [] },
  "companies": { "include": ["Acme Corp"], "exclude": [] },
  "salary_usd": { "min": 120000, "max": 250000 },
  "posted_after": "2026-07-01T00:00:00Z",
  "search_description": true,
  "include_fields": ["summary"],
  "include_facets": ["work_model", "sources"],
  "page": 1,
  "page_size": 25
}
```

- `queries`: entries are OR'd. A double-quoted entry is an exact contiguous
  title phrase. A `-` prefix excludes. Max 10 positive entries — exceeding
  that IS a real `400` (exclusions don't count).
- Array values are NOT comma-split: `["full-time,contract"]` is one literal
  value that matches nothing. Write `["full-time", "contract"]`.
- `companies` matches by company name, resolved server-side.

### Canonical filter values (both endpoints)

- work model: `remote` | `hybrid` | `onsite`
- employment type: `full-time` | `part-time` | `contract` | `internship` |
  `freelance` | `temporary`
- experience level: `intern` | `entry` | `mid` | `senior` | `lead` | `executive`
- `sources`: ATS ids (`greenhouse`, `lever`, `workday`, …) — an open set of
  ~106; treat as opaque strings.

Job objects return DISPLAY values (`Remote`, `Full-time`, `Senior`,
`Entry Level`) for these same concepts — never feed a display value back in as
a filter. The filter is `work_model`/`work_models`; the field on the job is
`workplace_type`. There is no `is_remote` parameter or field.

## Response envelope

```json
{
  "jobs": [ { "...": "job objects" } ],
  "total": 12847,
  "page": 1,
  "page_size": 25,
  "total_pages": 514,
  "facets": {
    "work_model": [ { "key": "remote", "count": 5230 } ],
    "sources": [ { "key": "greenhouse", "count": 3120 } ]
  }
}
```

Mirrored in headers: `X-Total-Count`, `X-Total-Pages`, `X-Page`, `X-Page-Size`.
Facet keys are canonical filter values — feed them straight back in as filters
to build the filter UI from live counts. Useful job fields for a results list:
`id`, `title`, `company.name`, `company.logo_url`, `locations`, `compensation`,
`workplace_type`, `employment_type`, `experience_level`, `date_posted`,
`listing_url`, `apply_url`. `description` is sanitized HTML — sanitize again
(e.g. DOMPurify) before rendering in a web UI.

## Three silent failure modes — design around them

1. An unknown PARAMETER is ignored, not rejected. `?is_remote=true` returns
   plausible-looking UNFILTERED results. Only use the parameter names in this
   prompt, spelled exactly.
2. An unknown filter VALUE matches nothing. `experience_level=senior level`
   returns `200` with `"total": 0`. Send canonical values only; when a filter
   is active and `total` is 0, treat it as a possible value bug, not just an
   empty result.
3. Deep pagination is CLAMPED, not rejected. Results are capped at a depth of
   10,000 (`page × page_size`); requests past it silently return the last
   reachable page. Cap our pagination UI at that depth. Bulk export must not
   page through search at all — that's what `POST /api/jobs/feed` is for (see
   Jobo's "Sync a database" guide); flag it if we need one, don't build it here.

## There is no sort parameter

Ordering is server-fixed: with a text query, relevance (then posted date);
without one, most recently updated first. Do not invent a `sort` param (it
would be silently ignored) and do not promise sortable columns in the UI.

## Field selection — keep responses small

`include_fields` controls the heavy, non-core fields: `description`, `summary`,
`qualifications`, `responsibilities`, `benefits`.

- Omit the parameter → full job objects (every field).
- Empty value (`?include_fields=` / `"include_fields": []`) → core fields only.
- A subset → core fields plus just those.

For a results list, request core fields only — `description` dominates payload
size. Fetch the full job for a detail view with `GET /api/jobs/{id}`, which is
FREE and returns the same job shape. Excluded fields come back as `""` / `[]`,
not absent. The same three-state rule applies to `include_facets` (allowed:
`work_model`, `experience_level`, `employment_type`, `sources`, `industries`,
`skills`; omit for the default low-cardinality set). Unknown names in either
parameter are silently dropped — spell them exactly.

## Errors, retries, and cost

- Retry ONLY 429, 500, 503. On 429/503 wait exactly the `Retry-After` header
  (seconds); on 500 back off exponentially (1s→30s, ±25% jitter); cap at 3–5
  attempts. Everything else: fix the request.
- `401` key problem, `402` out of credits or missing subscription — stop and
  surface, don't retry. Branch on status code + body fields, not Content-Type.
- Log the `x-correlation-id` response header on failures.
- Budget: search shares one per-key budget (`X-RateLimit-Group: JobSearch`) —
  60 requests/min free tier, 360/min subscribed. Metering is per DELIVERED
  job, so keep `page_size` at what the UI actually shows (10–25), debounce
  user input, and cache identical queries server-side for a few minutes if
  this repo has a cache. `GET /api/jobs/{id}` is free — never re-run a search
  to show a detail view.

## Deliverables

1. A search service module wrapping the two endpoints with the retry rules,
   typed to this repo's conventions.
2. Our own route/endpoint that proxies search (key stays server-side),
   validating `page_size` ≤ 100 and clamping page depth to 10,000.
3. If this repo has a frontend: a results view with pagination, facet-driven
   filters (checkboxes fed from `facets`), and empty-state handling that
   distinguishes "no matches" from "a filter value matched nothing".
4. Tests with mocked HTTP: canonical-value mapping (UI label → canonical key),
   the 10-positive-queries limit surfaced as a user error, retry honoring
   `Retry-After`, and the deep-pagination clamp.

## Acceptance checklist

- [ ] No `jobo-enterprise` / `Jobo.Enterprise.Client` dependency.
- [ ] Every filter value sent is a canonical lowercase key from this prompt.
- [ ] No invented parameters — no `sort`, no `is_remote`.
- [ ] Results list uses `include_fields` to skip `description`; detail views
      use free `GET /api/jobs/{id}`.
- [ ] Pagination UI capped at 10,000 depth; `page_size` ≤ 100.
- [ ] API key never reaches the browser.
````

***

## Audit an existing integration

For an app that **already calls the search API**. The silent failure modes mean a search integration can be wrong while returning `200` on every request — this prompt hunts for exactly that.

```markdown Audit-a-search-integration prompt [expandable] theme={null}
# Task: audit our Jobo search integration for silent failures

Our app calls the Jobo Enterprise search API. The API has failure modes that
return `200` while doing the wrong thing, so bugs here don't throw — they
quietly misfilter. Find our integration, audit it against the contract below,
and report findings before changing any code.

## Locate the integration

Search the codebase for: `connect.jobo.world`, `jobs-api.jobo.world`,
`/api/jobs`, `X-Api-Key`, `jobo_enterprise`, `jobo-enterprise`,
`Jobo.Enterprise.Client`. Map every call site before auditing.

## Contract reference (current, condensed)

- Base `https://connect.jobo.world` (HTTPS only), header `X-Api-Key`, JSON
  snake_case. Endpoints: `GET /api/jobs` (query-string),
  `POST /api/jobs/search` (JSON body), `GET /api/jobs/{id}` (free).
- Canonical filter values — the ONLY values that match:
  work model `remote|hybrid|onsite`; employment type
  `full-time|part-time|contract|internship|freelance|temporary`; experience
  level `intern|entry|mid|senior|lead|executive`. Jobs return display values
  (`Remote`, `Full-time`, `Senior`) which do NOT work as filters.
- GET params: `q`, `search_description`, `location` (single), `sources`,
  `work_model`, `employment_type`, `experience_level`, `skills`, `industries`
  (comma-separated), `posted_after/-before`, `discovered_after/-before`,
  `min_salary_usd`, `max_salary_usd`, `page`, `page_size` (1–100),
  `include_fields`, `include_facets`. POST body: same concepts as arrays
  (`work_models`, `employment_types`, `experience_levels`), plus
  `queries[]` (OR'd; quoted = exact title phrase; `-` excludes; max 10
  positive), `locations[]`, `companies`/`skills`/`industries` as
  `{include, exclude}`, `salary_usd` `{min, max}`.
- Response: `{jobs, total, page, page_size, total_pages, facets}`; facet keys
  are canonical filter values.
- Retryable: exactly 429/500/503, `Retry-After` authoritative on 429/503.
- Rate limits: `JobSearch` group, 60/min free, 360/min subscribed; metered per
  delivered job.

## Checklist — verify each item, citing file and line

### Critical — silent wrong results

1. **Invented parameters.** The API IGNORES unknown parameters. Grep our call
   sites for anything not in the contract above — `sort`, `order`, `is_remote`,
   `remote=true`, `seniority`, `job_type`, etc. Any hit means that "filter" has
   never done anything and users saw unfiltered results.
2. **Display values sent as filters.** `work_model=Remote` or
   `experience_level=Senior` returns `200` with `total: 0`. Check every place
   a filter value originates (UI labels, DB columns, config) and confirm the
   mapping to canonical lowercase keys.
3. **Client library below 4.0.0.** Those versions don't expose
   `search_description`, `posted_before`, `discovered_after`/`_before` or
   `include_fields`, and their enums are missing `freelance` and `intern`, so
   any call site needing those is either hand-rolling query strings or simply
   not using them. Report the pinned version of `jobo-enterprise` or
   `Jobo.Enterprise.Client`; 4.0.0 exposes all of them, and the upgrade is the
   smallest fix available.
4. **Comma-joined POST arrays.** On `POST /api/jobs/search`, array values are
   not comma-split: `["full-time,contract"]` is one literal value matching
   nothing. Check how we build arrays from multi-select UI state.
5. **Deep pagination assumptions.** Depth is clamped at 10,000
   (`page × page_size`) — silently. If we page through search to export or
   mirror jobs, that loop cannot see the whole corpus and burns credits;
   flag it for migration to `POST /api/jobs/feed` (cursor-based, cheaper).

### Important

6. Zero-result handling: when a filter is active and `total` is 0, do we
   distinguish "no matches" from "the filter value matched nothing"? Add
   logging/telemetry on filtered zero-result queries if absent.
7. `page_size` within 1–100 (out of range is a real `400`); `queries` capped
   at 10 positive entries (a real `400`).
8. Cost: results lists should use `include_fields` to exclude `description`;
   detail views should use free `GET /api/jobs/{id}` instead of re-searching;
   identical queries cached if this repo has a cache; input debounced.
9. Retry set exactly {429, 500, 503} with `Retry-After` honored; no retries on
   4xx validation errors.
10. `description` rendered through a sanitizer (it's sanitized HTML upstream,
    but defense-in-depth in a web UI) — and not double-escaped into visible
    tags.
11. API key server-side only — never shipped to a browser bundle; loaded from
    env/secret store, not hardcoded.

### Minor

12. Base URL `connect.jobo.world` (`jobs-api.jobo.world` is a legacy alias).
13. `x-correlation-id` logged on failures; pacing reads
    `X-RateLimit-Remaining` instead of reacting to 429s.
14. Facets: unknown names in `include_facets` are silently dropped — check
    spelling (`work_model`, `experience_level`, `employment_type`, `sources`,
    `industries`, `skills`).

## Report format

A table — severity, finding, file:line, user-visible impact, proposed fix —
followed by a short paragraph per critical finding. For findings 1–3, estimate
how long the bug has been live (git blame) and what users experienced. Then
STOP and wait for my approval before implementing fixes.
```

***

## After the assistant is done

Hold the result against [Search recipes](/docs/guides/search-recipes) — the human-readable version of the same contract. The things worth checking by hand:

* Every filter value sent is a canonical lowercase key, and no parameter appears that isn't in the docs.
* Detail views use `GET /api/jobs/{id}` (free), not a repeated search.
* Nothing installed `jobo-enterprise` or `Jobo.Enterprise.Client`.

<CardGroup cols={2}>
  <Card title="Search recipes" icon="magnifying-glass" href="/docs/guides/search-recipes">
    Working filter combinations, relevance behaviour, facets, and field selection.
  </Card>

  <Card title="The job object" icon="briefcase" href="/docs/api-reference/jobs/object">
    Every field, its type, nullability, and the enum send/receive table.
  </Card>
</CardGroup>
