OpenOpps

OpenOpps docs / Reference

Data Model

Sources, boards, providers, jobs, exports, static indices, and telemetry event-lake guidance.

CLI-onlylocal ledgerdata-model

OpenOpps keeps the operational ledger in local SQLite, publishes portable extracts for analysis, and serves the docs jobs/explorer surfaces from committed static artifacts. Use this page when you need to reason about data boundaries, counts, exports, or telemetry.

Core Entities

EntityMeaningPrimary producer
sourcesAggregate discovery catalogs such as a16z, accel, yc, public-company indices, or ecosystem landscapes.Packaged source adapters and plugins.
boardsFirm or company hiring boards discovered from sources.sources sync, boards sync, and admin board commands.
board_providersProvider route metadata between a board and a public job adapter.Provider detection, route probing, and board sync.
jobsStable posting identity and lifecycle fields.Successful provider-route job syncs.
job_versionsNormalized current or historical public posting content.Job sync content hashing.
job_payload_snapshotsDistinct raw listing/detail payload snapshots for replay and audit.Job sync payload hashing.
job_sync_runs and job_sync_observationsPer-route sync evidence for new, unchanged, changed, reopened, and closed outcomes.Provider-route job syncs.

Job storage separates durable posting identity from changing content. The jobs table tracks status, first/last seen timestamps, close time, and current hashes. job_versions stores normalized user-visible content with child tables for locations, skills, skill keywords, and responsibility or qualification bullets. Raw payload drift updates payload snapshots and observations without creating a new user-visible version when normalized content does not change.

Ingest surplus taxonomy (S1–S4)

Board ingestions capture more than first-class normalized columns. OpenOpps classifies the extra signal into four surplus classes:

ClassWhere it lives todayExamples
S1 Raw-buriedraw_listing, raw_detail, job_payload_snapshotsGreenhouse metadata, requisition_id, office trees
S2 Fetchable-not-calledBoard detail APIs not requested during syncGreenhouse pay_input_ranges, org /offices trees
S3 DerivableComputable from normalized textseniority, expanded skill catalog, daysOpen
S4 Sync-evidencejob_sync_runs, job_sync_observationsVelocity, churn, reopen, median days open

Promotion policy: move high-cardinality S1 keys into typed JobRecord fields or version.extra_payload (posting_kind, seniority, provider_extras); keep full raw payloads in SQLite/Kaggle only. The docs search index uses tiered detail shards—T2 full body for indexable SEO jobs, T1 metadata-only for other open jobs—and never commits payloadSnapshots.

Count Provenance

OpenOpps has two generated count families:

Count familySourceUsed by
Package catalog countsscripts/generate_docs_data.py writes docs/lib/generated/openopps-data.json.Source catalog, provider registry, export format, docs summary cards.
Snapshot/search countsscripts/generate_docs_search_index.py reads kaggle/openoppsdb.sqlite and writes docs/public/data/openopps-search/.Jobs workbench, /explorer, snapshot freshness, filters, row totals, and preview/detail shards.

Do not copy counts into prose unless the sentence explicitly names the snapshot date or package source. Prefer generated components and generated manifest values so the site does not drift when sources, providers, locations, departments, or exports change.

Exports

boards export and jobs export provide filtered extracts of normalized records:

uv run openopps boards export --provider ashbyhq --has-jobs --format jsonl --output /tmp/openopps-boards.jsonl
uv run openopps jobs export --remote Full --skill Python --format csv --output /tmp/openopps-jobs.csv
uv run openopps jobs export --source a16z --salary-min 150000 --format parquet --output /tmp/openopps-jobs.parquet
uv run openopps jobs export --status all --format sqlite --output /tmp/openopps-jobs.sqlite
FormatUse caseNotes
jsonlStreaming audit logs and line-oriented processing.Preserves normalized values as JSON records.
csvSpreadsheet review and lightweight exchange.Formula-leading strings are neutralized before writing.
parquetPolars, DuckDB, and analytics workflows.Empty exports should remain readable empty tables.
sqliteRelational handoff, filtered local extracts, and portable query files.Entity tables should use the flattened CSV/Parquet contract; nested values should be stable JSON strings.

The full OpenOppsDB snapshot remains the durable SQLite ledger. The docs and Kaggle bundle flows use kaggle/openoppsdb.sqlite as the local ignored input for static search artifacts and public dataset generation.

Static Jobs and Explorer Index

The docs app does not call provider APIs at runtime. It reads committed artifacts under docs/public/data/openopps-search/:

ArtifactPurpose
manifest.jsonSnapshot version, generated time, counts, chunks, facets, and provenance.
Job chunksSearchable latest job rows for the jobs workbench.
Detail shardsLarger posting detail payloads loaded only after a job is selected. T1 shards carry metadata only; T2 shards add a bounded plain-text body (≤4000 characters, HTML stripped).
Board/provider chunksExplorer coverage, route, and source/provider inspection.

Indexable job criteria

A job receives a T2 detail shard (and appears in jobs-indexable-ids.json) only when all of the following hold at index generation time:

  • status is open (or unset)
  • title, company, and a non-empty description (plain or HTML) are present
  • at least one of postedAt, firstSeenAt, or versionCreatedAt is set
  • a safe absolute postingUrl or applyUrl is present (no credentials, fragments-only, or non-http schemes)

All other open jobs still get T1 metadata-only detail shards for preview and filtering.

Refresh the index after updating kaggle/openoppsdb.sqlite:

just docs-search-index
just docs-search-index-check

just docs-search-index-check requires the ignored local SQLite snapshot, regenerates docs/public/data/openopps-search/, and fails if the committed search artifacts still differ. If the SQLite file is absent, use package-derived docs checks and record the missing snapshot as the blocker for search-index parity.

Facets and Suggestions

Jobs and explorer filters should be generated from the static search manifest, not hardcoded UI lists. The manifest is the right place for canonical values and counts for:

  • sources and source aliases
  • providers and support levels
  • locations
  • departments and teams
  • companies
  • skills and skill keywords
  • workplace and employment types
  • job status and salary currency

Seniority (seniorities facet): populated from each job row's seniority column. Values come from version.extra_payload.seniority when promoted during ingest; otherwise the index generator derives a label from normalized title and job_versions.experience text using the same derive_seniority() rules as openopps.models.

Days open (daysOpen column): computed at index generation from snapshotAt minus firstSeenAt (non-negative integer days). This is an S3 derivable field and is not stored in SQLite job rows.

Fuzzy matching should rank normalized generated suggestions first and only fall back to typed substring matching when the user enters a value that is not in the generated index.

Provider surplus promotion

Provider adapters preserve full upstream payloads on raw_listing and raw_detail. v0.1 promotion focuses on S1 list-endpoint fields already fetched during sync:

ProviderPromoted surplus (initial)
Greenhousemetadata, requisition_id, language, department/office trees; posting_kind=prospect when internal_job_id is null
WorkableSplit raw_listing (list POST) vs raw_detail (per-job GET)
OthersSee openspec/changes/ingest-data-surplus/provider-promotion-manifest.json

Derived S3 fields such as seniority are computed from title/experience text without extra HTTP. S4 sync aggregates surface in the search manifest dashboard.sync block when job_sync_runs is present in the snapshot.

Telemetry Event Lake

The docs app telemetry source of truth should be a first-party event lake. The collector can run as a no-op locally, append raw events when persistent storage is configured, mirror sanitized events to PostHog when configured, and compact local events into Parquet for DuckDB analysis.

Recommended event shape:

FieldMeaning
event_idClient-generated unique id for de-duping.
schema_versionTelemetry schema version, starting at 1.
sent_at and received_atClient and collector timestamps.
event_nameAllowlisted event name such as jobs.filters_changed or explorer.lineage_loaded.
anonymous_id, session_id, page_idFirst-party anonymous identifiers.
context.path, context.viewport, context.screen, context.timezone, context.language, context.languages, context.userAgentBrowser context retained by the current route allowlist. Raw search, url, and referrer fields are dropped.
request.path, request.headers, request.ipServer-side request context after header allowlisting, query removal, and IP drop/hash/raw policy.
entity_contextJob, source, provider, filter, or explorer entity ids when relevant.
propertiesEvent-specific allowlisted metadata.
redaction_count, payload_truncatedEvidence that sanitization and size limits were applied.

page_engagement events include both cumulative counters (durationMs, visibleDurationMs, interactionCount) and additive counters (durationDeltaMs, visibleDurationDeltaMs, interactionDeltaCount) with a reason and sequence. Use the additive counters for aggregate dwell-time stories so route changes, hidden-tab flushes, and unmounts do not double-count the same page interval.

Runtime sink modes:

SinkUse caseCost posture
noopLocal development or deployments without storage.Free; records nothing.
local-event-lakeCanonical first-party telemetry on persistent disk.Free when running on existing infrastructure.

The current /api/telemetry route treats any unsupported sink value as noop. PostHog forwarding is controlled separately with OPENOPPS_POSTHOG_PROJECT_API_KEY, receives only allowlisted sanitized properties, and is bounded by OPENOPPS_POSTHOG_TIMEOUT_MS. Browser PostHog session replay is an opt-in mirror controlled by NEXT_PUBLIC_OPENOPPS_POSTHOG_PROJECT_API_KEY; replay masks text and inputs, disables network body/header capture, respects PostHog project sampling and trigger controls, and does not change the first-party event schema. Cloudflare, Umami, or BI dashboards can mirror sanitized summaries downstream, but the canonical raw sink remains local-event-lake.

Sanitize obvious secret-like values, cap event size, and make raw IP retention an explicit opt-in. Hashing IPs with a deployment salt is the default compromise when approximate abuse/debugging signal is useful.

On this page