The prompts call the API over raw HTTPS, so your code gains no dependency. The 4.0.0 client libraries cover
updated_after, stable_scan, the managed feed, and expired IDs. They lack saved feeds and the newer filters (full list).Pick your prompt
| Your situation | Use |
|---|---|
| No integration yet: build the sync into your platform | Build a feed sync |
| You already sync the feed: check it against the current contract | Audit an existing integration |
| You’re on a client library older than 4.0.0 | Upgrade the client library |
1
Paste a prompt into your assistant at the root of your repo
Claude Code, Cursor, Copilot, or any assistant that can read and edit your codebase. The copy button on each prompt copies all of it.
2
Answer its questions, then review the diff
Each prompt makes the assistant report on your codebase before it changes any code. The build and upgrade prompts end with an acceptance checklist.
Build a feed sync
The assistant builds three pieces in your existing stack: a one-time backfill, an incremental sync every 15–60 minutes, and an expired-job sweep in the same run. To narrow the feed, pick filters on the Feed Builder page. Paste its request body into the prompt in place of the example body underPOST /api/jobs/feed.
Build-a-feed-sync prompt
# Task: integrate the Jobo job feed into this codebase
Mirror Jobo's job corpus into this platform's own datastore and keep it current.
Three pieces: (1) a one-time backfill, (2) a scheduled incremental sync, (3) an
expired-job sweep.
## First: inspect this repo and confirm the plan
Before writing any code, examine the codebase and report back:
- Language(s) and framework
- The HTTP client already in use
- The database, and the migration tooling for it
- The scheduling mechanism (cron, job queue, hosted scheduler, …)
- How config and secrets are handled
- Logging conventions
Propose where the new code will live, following this repo's conventions, and
wait for my confirmation. Use the existing stack — do not introduce a new HTTP
client, ORM, or scheduler if one exists. 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, but 4.0.0 does not cover every filter below; we are choosing
not to add one here.) Do not add an HTTP wrapper of your own beyond the
shared helper the deliverables ask for.
2. Read the API key from the `JOBO_API_KEY` environment variable (or this
repo's existing secret mechanism). Never hardcode it, log it, or commit it.
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 guessed name will appear to work.
4. Paginate serially — never fan out cursor requests in parallel.
## API basics
- Base URL: `https://connect.jobo.world`. HTTPS only — plain HTTP gets a `301`
and most clients drop a POST body on redirect.
- Auth: `X-Api-Key: <key>` header on every request. There is no
`Authorization: Bearer` form and no query-parameter auth.
- All JSON is snake_case in both directions. No version segment in any path.
- Send `Content-Type: application/json` on POSTs.
- Configure a response timeout of at least 120 seconds — the feed can spend up
to ~90s server-side before returning a retryable `503`.
## Endpoints
### POST /api/jobs/feed — the bulk feed (metered)
Request body (first request of a scan only — see cursor rules):
```json
{
"batch_size": 1000,
"updated_after": "2026-07-27T06:00:00Z",
"posted_after": null,
"sources": ["greenhouse", "lever"],
"work_models": ["remote"],
"employment_types": ["full-time"],
"experience_levels": ["senior"],
"locations": [{ "country": "US", "region": null, "city": null }],
"stable_scan": true
}
```
- `batch_size`: 1–1000, default 1000. Always use 1000.
- `updated_after` (ISO 8601, optional): the incremental watermark — returns jobs
created OR updated on/after it, including jobs that expired and were
reactivated (reactivation re-indexes them).
- `posted_after` (optional): matches the employer posting time or Jobo's
first-indexed time. NOT a sync watermark — it misses edits.
- `include_reactivated` (optional, default false): only for `posted_after`
queries — also returns jobs reactivated on/after `posted_after`, which keep
their original posting date. Do NOT set it for `updated_after` syncs; they
already include reactivated jobs.
- `sources` (optional): ATS platform ids (`greenhouse`, `lever`, `workday`, …).
An open set of 200+ values — treat as opaque strings.
- `work_models` (optional): `remote` | `hybrid` | `onsite`
- `employment_types` (optional): `full-time` | `part-time` | `contract` |
`internship` | `freelance` | `temporary`
- `experience_levels` (optional): `intern` | `entry` | `mid` | `senior` |
`lead` | `executive`
- `languages` (optional): ISO 639-1 codes of the description language,
e.g. `["en", "de"]`
- Occupation and company filters (optional), each `{"include": [...],
"exclude": [...]}`, case-insensitive: `job_categories`, `job_subcategories`,
`occupation_ids` (the job's `occupation`; values from `GET /api/occupations`,
prefer ids), `industries`, `company_primary_industries`,
`company_categories`, `company_sizes` (`1-10` | `11-50` | `51-200` |
`201-500` | `501-1000` | `1001-5000` | `5001-10000` | `10001+`) and
`company_revenues` (`under-1m` | `1m-10m` | `10m-50m` | `50m-100m` |
`100m-200m` | `200m-1b` | `over-1b`). Jobs or companies without a value never
match an `include` list. An unknown size or revenue band is a `400`.
- `company_is_agency` (optional boolean): `false` leaves out jobs posted by
recruitment and staffing agencies; `true` returns only theirs.
- `exclude_sources`, `exclude_work_models`, `exclude_employment_types`,
`exclude_experience_levels`, `exclude_languages` (optional): leave out jobs
with any of these values, normalized like the include list of the same name.
Jobs without a value are kept. May be combined with the include list, e.g.
`"sources": ["greenhouse", "lever"], "exclude_sources": ["lever"]`.
- `companies` (optional): `{"include": [...], "exclude": [...]}` of Jobo
company ids, website domains (`fedex.com`) or whole company names (`FedEx`),
matched as on `POST /api/jobs/search`. Matched on the first page; the cursor
keeps the matched company ids. Unmatched entries are NOT a `400`: they come
back in the first page's `warnings`. When no `include` entry matches (or
every match is outside `sources`), the first page is empty (`jobs: []`,
`has_more: false`, `next_cursor: null`) and is not billed.
- `locations` (optional): array of `{country, region, city}` objects, ORed.
`country` accepts full names or country codes (`US`, `USA`, `GB`, `UK`, `DE`),
case-insensitively. Codes are countries in this field: `CA` means Canada.
City takes precedence over region, then country; use full city and region
names. For example, `{"country":"US","region":"California"}` filters California.
- `stable_scan`: leave at its default `true`.
- `description_format` (optional): `markdown` (default) or `html`. Unlike the
filters, it is read from every request, so send it on continuations too.
- Every field is optional. A full-corpus mirror sends only `{"batch_size": 1000}`.
Response:
```json
{
"jobs": [ { "...": "Job objects, schema below" } ],
"next_cursor": "opaque-string-or-null",
"has_more": true,
"estimated_total": 123456,
"warnings": [
{ "code": "companies_unmatched", "field": "companies.include",
"message": "1 of 2 names in companies.include matched no company (\"Glenmark\")." }
]
}
```
`estimated_total` appears on the first page of a scan only. `warnings` appears
on the first page only, and only when non-empty: codes `companies_unmatched`,
`companies_exclude_unmatched`, `companies_outside_sources` (the matched
companies are on none of the requested `sources`), repeated comma-separated in
the `X-Jobo-Warnings` header. Log them; they don't fail the request.
### GET /api/jobs/expired — recently expired job ids (free)
Query parameters: `expired_since` (ISO 8601; omitted = last 24h; must be within
the last **7 days** and not in the future), `batch_size` (1–10000 — use 10000),
`cursor`. A cursor this endpoint cannot decode is ignored, not rejected: you get
the first page again.
```json
{ "job_ids": ["uuid", "uuid"], "next_cursor": "or-null", "has_more": false }
```
### GET /api/jobs/{id} — one job by id (free)
Same job schema. Useful for spot checks and tests.
## Cursor rules (apply to every cursor endpoint)
1. Send filters and `batch_size` on the FIRST request only. The cursor embeds
them — a continuation request carries ONLY `{"cursor": "..."}` (or the
`cursor` query param on GET /api/jobs/expired). Filters sent alongside a
cursor are silently ignored, and `batch_size` can be lowered but never
raised. The one exception is `description_format`: if you use it, send it
with the cursor on every continuation.
2. One in-flight request per cursor. Continue serially until `has_more` is false.
3. Persist `next_cursor` after every successfully processed batch, so a crash
resumes mid-stream instead of restarting from page one. Cursors do not
expire, so a stored checkpoint can resume at any time.
## The job object → our schema
Upsert on `id` — a stable UUID that survives edits to the posting. Store
`updated_at` per row and skip the write when the incoming value is unchanged.
Nullable fields arrive as `null`; array fields arrive as `[]` (never null).
The optional `external_id` is omitted when unavailable. Store it as text to
preserve leading zeros; it is not the upsert key or necessarily an employer
requisition number. `occupation` is always present and `null` when the job
is not classified; store it as a nullable column.
| Field | Type | Nullable | Notes |
| --- | --- | --- | --- |
| `id` | uuid | no | Primary key for the upsert |
| `external_id` | string | optional | Collected ATS/job-board job identifier; omitted when unavailable; not globally unique |
| `title` | string | no | |
| `normalized_title` | string | yes | snake_case canonical title, e.g. `software_engineer` |
| `occupation` | object | yes | Null when unclassified. `{id, name, category, subcategory, isco_code?, onet_soc_codes[]}`, e.g. `{"id": "esco:8d3e8aaa-791b-4c75-a465-f3f827028f50", "name": "Registered Nurse", "category": "Healthcare", "subcategory": "Nursing", ...}`. Store as JSON/JSONB or flattened columns; key on `id`, since names can change |
| `company` | object | no | `{id (uuid), name, website?, logo_url?, summary?, industries[], categories[], linkedin_url?, crunchbase_url?, details_url?}` |
| `description` | string | no | **Markdown by default**: line breaks preserved, light Markdown (bullets, bold, links). Send `"description_format": "html"` on every request to receive HTML instead. Store as-is; sanitize any HTML you render (e.g. DOMPurify) |
| `summary` | string | yes | AI summary; null while pending on fresh jobs |
| `listing_url` | string | no | |
| `apply_url` | string | no | |
| `locations` | object[] | no (may be `[]`) | Each: `{location?, city?, region?, country?, latitude?, longitude?}` |
| `compensation` | object | yes | `{min?, max?, currency? (ISO 4217), period?}`; `period` ∈ `yearly` `monthly` `weekly` `daily` `hourly` `per-diem` |
| `employment_type` | string | yes | Display value, e.g. `Full-time` |
| `workplace_type` | string | yes | Display value: `Remote` \| `Hybrid` \| `On-site` |
| `experience_level` | string | yes | Display value, e.g. `Entry Level` |
| `language` | string | yes | ISO 639-1 code of the description, e.g. `en` |
| `source` | string | no | ATS id. Open set — store as text, never a DB enum or check constraint |
| `created_at`, `updated_at` | datetime | no | |
| `date_posted`, `valid_through` | datetime | yes | |
| `qualifications` | object | no | `{must_have, preferred}`, each `{education[], certifications[], skills[{name, type: "hard"|"soft"}]}` |
| `responsibilities` | string[] | no | |
| `benefits` | string[] | no | |
| `is_work_auth_required` | boolean | yes | |
| `is_h1b_sponsor` | boolean | yes | |
| `is_clearance_required` | boolean | yes | |
Enum gotcha: request filters take the lowercase canonical keys shown under the
endpoint above (`remote`, `full-time`, `senior`), but job objects return display
values (`Remote`, `Full-time`, `Senior`). Never feed a display value back into a
filter — it silently matches nothing. The filter is named `work_models`; the
field on the job is `workplace_type`. There is no `is_remote` parameter or field.
## The sync algorithm
### 1. Backfill (run once)
Record the start time as the first incremental watermark BEFORE the first
request. POST /api/jobs/feed with `{"batch_size": 1000}` (plus any filters we
agree on), loop under the cursor rules, upsert every job by `id`, checkpoint
`next_cursor` after each batch, stop when `has_more` is false.
### 2. Incremental sync (scheduled every 15–60 minutes)
Pick a cadence that fits this repo's scheduler. Running more often than every
15 minutes surfaces little new data, and each run's 15-minute overlap
re-delivers (and re-bills) jobs we already hold.
a. Read stored `last_run_started_at`. Record now as `this_run_started_at`
BEFORE the first request.
b. Start a new scan: `{"batch_size": 1000, "updated_after": last_run_started_at
minus 15 minutes}`. The 15-minute overlap absorbs clock skew and late
indexing; upserts make it idempotent.
c. Page to completion under the cursor rules. Upsert by `id`; skip rows whose
stored `updated_at` is unchanged.
d. Only after the loop fully succeeds, persist `this_run_started_at` as the new
`last_run_started_at`. Never advance the watermark on a failed run.
### 3. Expiry sweep (same schedule, same run)
The feed returns ACTIVE jobs only. An expired job never comes back with a flag —
it simply stops appearing. So after each incremental run:
GET /api/jobs/expired?expired_since=<last_run_started_at>&batch_size=10000,
page with the cursor, and mark every returned id expired/inactive in our
datastore (soft-flag rather than delete, unless this repo prefers hard deletes).
Constraint: `expired_since` must be within the last 7 days. If our sync stalls
longer than that, expired jobs become unrecoverable through this endpoint.
Detect the case (watermark older than 7 days) and recover: run a full backfill
again, then mark as expired every stored id that no longer appeared.
## Errors and retries
- Retry ONLY `429`, `500`, `503`. Anything else is a defect in the request —
fix it, don't retry it.
- `429` / `503`: wait at least the `Retry-After` header value (seconds). Cap at
3–5 attempts.
- `500`, or a `503` without `Retry-After`: exponential backoff from 1s,
doubling to a 30s cap, with ±25% jitter, 3–5 attempts.
- `400` with `"code": "invalid_cursor"`: drop the cursor and restart the scan
from the beginning. Never resend the same cursor.
- `409` with `"code": "feed_cursor_restart_required"`: restart the scan with
`{"stable_scan": true, "batch_size": 1000}`.
- `401`: key problem — stop and alert. `402`: the remaining included jobs
plus wallet cannot cover the request (`"error": "Insufficient credits"`) —
stop and alert; retrying without funding or a smaller batch cannot help.
- `404` with `"code": "not_available_in_sandbox"`: the key is a sandbox key
(`jbe_test_…`), which cannot call the feed or expired endpoints. Use a live
key (`jbe_live_…`).
- Branch on the status code and body fields (`code` where present, else the
`error` string) — NOT on Content-Type, which varies between
`application/json` and `application/problem+json`.
- Log the `x-correlation-id` response header on every failure — Jobo support
needs it to trace the request.
## Rate limits and cost
- Feed endpoints share one account-wide budget across all your keys
(`X-RateLimit-Group: JobFeed`) that scales with your plan — from 10
requests/min and 1,000/day without a subscription up to 60/min and
10,000/day on Scale or Unlimited (full table:
https://jobo.world/docs/rate-limits). Read `X-RateLimit-Remaining` and slow
down when it drops below ~15% of `X-RateLimit-Limit` instead of bouncing off
429s.
- Feed uses a Job Search plan's included jobs first, then the pay-as-you-go
rate, and has no per-job charge on Unlimited. Metering is per delivered job, so
always use `batch_size: 1000` — larger batches cost no more per job and use a
tenth of the request budget. `GET /api/jobs/expired` and `GET /api/jobs/{id}`
are free.
- Surface the `X-Credits-Balance` response header and alert when it runs low.
## Deliverables
1. A migration for the jobs table plus sync state (`last_run_started_at`, the
in-flight cursor checkpoint), following this repo's migration conventions.
2. The sync module: backfill entrypoint, incremental entrypoint, expiry sweep,
and one shared HTTP helper implementing the retry rules above.
3. Scheduling wired into this repo's existing mechanism (every 15–60 minutes).
4. Tests with mocked HTTP covering: the cursor loop resuming from a checkpoint,
watermark-advances-only-on-success, expiry marking, `Retry-After` honored on
429/503, and `invalid_cursor` causing a restart rather than a retry loop.
5. A short README section: required env vars, how to run the backfill, and how
the 7-day recovery path works.
## Acceptance checklist — verify every item before calling it done
- [ ] No `jobo-enterprise` / `Jobo.Enterprise.Client` dependency anywhere.
- [ ] Continuation requests carry only the cursor (plus `description_format`,
if used).
- [ ] The watermark advances only after a fully successful run.
- [ ] The expiry sweep runs on the same schedule as the incremental sync, and a
stall past 7 days triggers (or at minimum flags) a full re-backfill.
- [ ] Upserts key on `id`; unchanged `updated_at` skips the write.
- [ ] Retries cover exactly 429/500/503, honor `Retry-After`, and restart on
`invalid_cursor` / `feed_cursor_restart_required`.
- [ ] Client timeout ≥ 120s, API key from env, HTTPS URLs only, serial
pagination.
Audit an existing integration
The assistant finds every file that calls Jobo and checks it against 16 items, ranked critical, important, or minor. It reports each finding with its file and line, and changes no code until you approve the report.Audit-a-feed-integration prompt
# Task: audit our Jobo job-feed integration against the current API contract
Find our integration, verify it against the contract below, and produce a
findings report. Do not change any code until I approve the report.
## Locate the integration
Search the codebase for: `connect.jobo.world`, `jobs-api.jobo.world`,
`/api/jobs/feed`, `/api/jobs/expired`, `X-Api-Key`, `jobo_enterprise`,
`jobo-enterprise`, `Jobo.Enterprise.Client`. Map every file that touches the
Jobo API before auditing.
## Contract reference (current, condensed)
- Base `https://connect.jobo.world` (HTTPS only; `jobs-api.jobo.world` is a
retired hostname that no longer resolves). Auth header `X-Api-Key`. All JSON
snake_case.
- `POST /api/jobs/feed` — body on the FIRST request of a scan:
`batch_size` (1–1000), optional `updated_after`, `posted_after`, `sources[]`,
`work_models[]` (`remote|hybrid|onsite`), `employment_types[]`
(`full-time|part-time|contract|internship|freelance|temporary`),
`experience_levels[]` (`intern|entry|mid|senior|lead|executive`),
`locations[]` (`{country, region, city}`; country accepts names or codes such
as `US` or `USA`, case-insensitively), `stable_scan` (default true), and
`{include[], exclude[]}` filters `job_categories`, `job_subcategories`,
`occupation_ids`, `industries`, `company_primary_industries`,
`company_categories`, `company_sizes`, `company_revenues`, `companies`
(ids, domains or whole names), plus `languages[]` (ISO 639-1), boolean
`company_is_agency`, `exclude_sources[]`, `exclude_work_models[]`,
`exclude_employment_types[]`, `exclude_experience_levels[]`,
`exclude_languages[]`, `include_reactivated` (with `posted_after` only) and
`description_format` (`markdown` default, or `html`).
Response `{jobs[], next_cursor, has_more, estimated_total?, warnings?}`
(`warnings` first page only). Continuation requests carry ONLY
`{"cursor": ...}` — filters sent alongside are silently ignored;
`description_format` is the exception and is read on every request.
- `GET /api/jobs/expired?expired_since=...&batch_size=<=10000&cursor=...` —
free; `expired_since` max 7 days back, default 24h. Response
`{job_ids[], next_cursor, has_more}`.
- `GET /api/jobs/{id}` — free single-job lookup.
- Retryable statuses: exactly 429, 500, 503. On 429/503 wait at least
`Retry-After` (seconds); on 500, or a 503 without it, back off exponentially
(1s doubling to 30s, ±25% jitter), 3–5 attempts. `400 invalid_cursor` →
restart the scan.
`409 feed_cursor_restart_required` → restart with
`{"stable_scan": true, "batch_size": 1000}`.
- Rate limits: `JobFeed` group, 10/min with no subscription, 20–60/min by plan
(60/min on Scale and Unlimited), plus hourly and daily windows; headers
`X-RateLimit-Limit/-Remaining/-Used/-Group`. Metered per delivered job;
`X-Credits-Balance` on debits. The feed's server-side budget is ~90s, so
client timeouts must be ≥ 120s.
- Job objects: `id` is a stable UUID (the upsert key). `updated_at` changes on
edits. `description` is Markdown by default (HTML with
`"description_format": "html"`). Filters take lowercase canonical values;
job fields return display values (`Remote`, `Full-time`, `Senior`).
`source` is an open set of 200+ ids. Nullable fields arrive as `null`,
arrays as `[]`. The optional `external_id` is omitted when unavailable;
preserve it as text and keep `id` as the upsert key.
## Checklist — verify each item, citing file and line
### Critical
1. **Client library below 4.0.0.** `jobo-enterprise` (PyPI/npm) and
`Jobo.Enterprise.Client` (NuGet) before 4.0.0 have no `updated_after` on
feed methods, so any sync built on them is re-scanning rather than syncing
incrementally. Their enums are also missing `freelance` and `intern`. Report
the pinned version; 4.0.0 fixes both, and the upgrade is the smallest fix
available.
2. **No expiry sweep.** The feed returns active jobs only; expired jobs simply
stop appearing. If nothing calls `GET /api/jobs/expired` on the same
schedule as the sync, our datastore is accumulating dead rows. Quantify if
possible (jobs in our store not updated in > 14 days).
3. **Wrong watermark.** Incremental sync must use `updated_after` (with a
~15-minute overlap), not `posted_after` (misses edits) and not a full
re-scan every run (wastes credits). The watermark must be recorded BEFORE
the run's first request and persisted only AFTER the run fully succeeds.
4. **Cursor misuse.** Continuation bodies must carry only the cursor (filters
sent alongside are ignored — if we "change filters mid-scan", it never
worked). Pagination must be serial, and `next_cursor` checkpointed so a
crash resumes.
5. **Display values used as filters.** Filters must send canonical lowercase
values (`remote`, `full-time`, `senior`). Sending `Remote` or
`Senior` returns 200 with zero results — silently.
6. **The 7-day expiry window unhandled.** `expired_since` older than 7 days is
rejected. If our sync can stall past 7 days with no full-backfill recovery
path, dead rows become permanent.
### Important
7. Retry set is exactly {429, 500, 503}; `Retry-After` honored; `invalid_cursor`
restarts rather than loops; `feed_cursor_restart_required` handled.
8. Client timeout ≥ 120 seconds.
9. Upserts key on `id`; unchanged `updated_at` skips the write.
10. `batch_size` is 1000 on the feed (metering is per delivered job — small
batches waste the request budget) and up to 10000 on the expired sweep.
11. API key comes from env/secret store, not source; all URLs are `https://`.
12. `description` handled as Markdown (or HTML, if we send
`description_format: html` on every request) — stored as-is, and any HTML
rendered from it passes through a sanitizer in web UIs.
13. `source` stored as open text — no DB enum or check constraint that a new
ATS id would violate.
### Minor
14. Base URL is `connect.jobo.world`. Any call site still on the retired
`jobs-api.jobo.world` is failing outright (the hostname no longer
resolves) — report it as critical.
15. Pacing reads `X-RateLimit-Remaining` instead of reacting to 429s;
`x-correlation-id` is logged on failures.
16. Null vs empty-array semantics handled per the contract.
## Report format
A table — severity, finding, file:line, impact, proposed fix — followed by a
short paragraph per critical finding. If historical damage is plausible
(zero-result filters, missing expiry sweeps), say what a recovery run looks
like. Then STOP and wait for my approval before implementing fixes.
Upgrade the client library
jobo-enterprise and Jobo.Enterprise.Client below 4.0.0 have no updated_after, so a sync built on them re-reads the whole feed every run. The assistant upgrades the dependency to 4.0.0 and moves the sync onto updated_after. It also adds an expiry sweep if one is missing and checks stored data for damage from the old enums. The prompt covers the search changes too.
To drop the dependency instead, use Build a feed sync.
Upgrade-the-client prompt
# Task: upgrade the Jobo client library to 4.0.0 in this codebase
We use an official Jobo client — `jobo-enterprise` (PyPI or npm) or
`Jobo.Enterprise.Client` (NuGet) — pinned below 4.0.0. Upgrade it, adopt the
capabilities the old version could not express, and check whether the old
version's enum values corrupted anything we stored.
## Step 1 — inventory
Report before changing anything:
- The pinned version in the manifest and lockfile.
- Every import/usage of `jobo_enterprise`, `jobo-enterprise`, or
`Jobo.Enterprise.Client`, with the method called and the parameters passed.
- Whether our feed sync re-scans everything each run or syncs incrementally.
## Step 2 — what changes in 4.0.0
Breaking:
- `client.auto_apply` / `client.autoApply` / `client.AutoApply` **no longer
exists**. It targeted routes the API removed, so nothing on it worked. If we
call it, remove those call sites. The current Auto Apply contract is
synchronous and has its own TypeScript client, `@jobo-ai/autoapply`; see
Jobo's Auto Apply docs for access.
- `EmploymentType` members now serialize `full-time` / `part-time` rather than
`full_time` / `part_time`. Both spellings match on the wire, so this changes
what we send, not what we get back.
- `CompensationPeriod` members were renamed to the values the API actually
emits — `hourly`, `daily`, `weekly`, `monthly`, `yearly`, `per-diem`. The old
members (`hour`, `day`, `week`, `month`, `year`) matched none of them.
- Node only: if we are on 1.x, the surface moved from flat methods
(`searchJobs`, `getJobsFeed`) to sub-clients (`client.search.search(...)`,
`client.feed.getJobs(...)`).
New, and worth adopting:
- Feed: `updated_after`, `stable_scan`, `employment_types`, `experience_levels`.
- Search: `search_description`, `posted_before`, `discovered_after`,
`discovered_before`, `include_fields`.
- `get_job(id)` for a single job — free.
- The managed feed (`get_managed_jobs` / `iter_managed_jobs`), if we use Managed
Job Scraping. Do not pass `sources` to it: the managed-feed endpoint does not
accept that field and silently drops it. The method also lacks `locations`,
`employment_types`, `experience_levels` and `languages`, which the endpoint
does accept; send those over raw HTTPS.
- `expired_since` is now optional and defaults to a 24-hour lookback.
- `429` and `503` are retried internally with bounded backoff honoring
`Retry-After`, and `403` / `404` / the `409` cursor restart raise distinct
typed errors carrying the API's problem `code`.
Not in 4.0.0 — keep raw HTTPS for these: saved feeds
(`POST /api/jobs/feed/{feed_id}`), `GET /api/occupations`,
Company Enrichment (`GET /api/companies/by-domain`), and parameters added since
4.0.0 (search `sort`, `language`, `company_categories`, the occupation filters
and `description_format`; feed `languages`, `exclude_*`, `companies`,
`industries`, the occupation and `company_*` filters, `include_reactivated`
and `description_format`).
## Step 3 — check for latent damage
- **Stored compensation periods.** If we mapped API responses through the old
`CompensationPeriod` members, stored values may be wrong — the old members
matched nothing the API emits. Check what is actually in our column.
- **Missing filter values.** The old enums had no `freelance` or `intern`. If a
UI offers those filters, confirm they were passed as raw strings and not
silently dropped.
- **Re-scanning.** Without `updated_after`, an incremental sync built on the old
version was re-reading the whole feed every run. Quantify the wallet cost if
you can.
## Step 4 — implement
1. Bump the dependency to `4.0.0` in the manifest and lockfile.
2. Fix the breaking changes from Step 2 so the project builds and tests pass.
3. Move the feed sync to `updated_after = last run start − 15 minutes`, with the
watermark persisted only after a fully successful run. Leave `stable_scan` at
its default.
4. Add a `GET /api/jobs/expired` sweep on the same schedule if one is missing —
the feed returns active jobs only, so expired rows never disappear on their
own. `expired_since` looks back 7 days at most.
5. Replace any hand-rolled HTTP that existed only to reach a parameter the old
client lacked and 4.0.0 now exposes. Keep it for anything 4.0.0 still
lacks (listed at the end of Step 2).
## Acceptance
- [ ] The manifest and lockfile pin 4.0.0.
- [ ] No reference to the removed Auto Apply sub-client anywhere.
- [ ] Incremental sync uses `updated_after`, not `posted_after` and not a full
re-scan.
- [ ] An expiry sweep runs on the same schedule as the sync.
- [ ] A written note on any latent damage found in Step 3.
After the assistant is done
Whichever prompt you used, check these by hand:- Continuation requests carry only the cursor (plus
description_format, if you use it). - The watermark advances only after a successful run, and an expiry sweep runs on the same schedule.
- Filters send canonical lowercase values (
remote,full-time), never the display values jobs return (Remote,Full-time).
jbe_test_…) can’t call the feed or expired endpoints (404 not_available_in_sandbox), so test the sync with mocked HTTP and run it on a live key.
Sync a database
The same contract in prose: backfill, incremental sync, expiry sweeps, and working Python.
The job object
Every field, its type, nullability, and the enum send/receive table.

