What you'll build: the two systems every company of any size pays for — an HRIS (the Workday-style HR core: workers, positions, the org chart) and an ATS (the Greenhouse-style recruiting funnel: jobs, candidates, applications) — as one DataK3 bucket. The reporting line is a graph you traverse, the resumes are a VECTOR column you rank by meaning, an Ignite app screens each new application, and a Models call runs the knock-out gate. One copy of the rows, queried by content, by relationship, and by similarity — no Postgres + Neo4j + Pinecone to stitch together.

What you'll learn:

  • Model the HR core + recruiting funnel as merge-keyed DataK3 tables — idempotent upserts, one store.
  • Turn the org chart into a graph and traverse it: everyone under a manager, the reporting chain to the top, and span-of-control — with graph_khop / graph_shortest_path that hydrate names in one statement.
  • Rank candidates for an open req by resume similarity — a VECTOR column and pgvector <=>, no store.
  • Deploy an Ignite screening engine (real handler.py, its own service account) that writes a match_score + shortlist flag back to applications.
  • Run a Models knock-out gate that reads structured hard requirements and writes a screen_result.

The problem — and why it matters

You run People Ops and Talent for a growing company. Today that means three vendors and three data models. Workday (or BambooHR, or SAP SuccessFactors) is the HR core — workers, positions, the org chart — and it bills per employee per year. Greenhouse (or Lever, or Ashby) is the ATS — jobs, candidates, applications — and it bills per seat. And the moment you want "rank these 200 applicants by how well their résumé fits the req," you're standing up a fourth system: a vector database like Pinecone, plus the glue that copies résumés into it and joins the hits back to your candidate records.

The org chart is the sharpest example of the mismatch. "Who is in Alan's org?", "what's Margaret's reporting line to the CTO?", "which managers have too wide a span of control?" are graph questions, but your HR core stores them as a manager_id foreign key — so you answer them with recursive CTEs, or you buy a fifth system (Neo4j) and sync the org into it nightly.

This tutorial collapses all of that into one bucket. The HR core and the funnel are SQL tables. The reporting line is a real graph over the same worker rows. Résumés are a vector column next to the candidate. A recruiter opens a req and gets a ranked, pre-screened shortlist — because the ranking and the screening run where the data already lives, not in a system you had to wire up.

Prerequisites

  • A DODIL organization, and the dodil CLI authenticated (dodil auth login) or the DODIL MCP connected to your agent (Claude Code, Cursor, VS Code, Codex).
  • export BUCKET=blog-hr — one bucket is the whole platform's data plane.

Every DataK3 op below shows two paths: an Ask your agent tab (the default — DataK3 is agent-native) and the CLI. Pick either.

Step 1 — Stand up the HR core + recruiting funnel (SQL)

The platform is the schema: five merge-keyed tables in one bucket. A PRIMARY KEY (merge-key) is required — writes are keyed, so re-imports from your payroll or ATS feed upsert idempotently instead of duplicating. workers holds a position_id (the seat they fill) and a manager_id (who they report to); jobs are open reqs; candidates carry the résumé; applications is the funnel row that ties a candidate to a job.

You

Create a DataK3 bucket blog-hr, then five merge-keyed tables: workers (key worker_id) with full_name, email, title, position_id, manager_id, department, location, level, hire_date, status; positions (key position_id) with title, department, level, location, job_family, filled (boolean), worker_id; jobs (key job_id) with title, department, location, level, description, status, hiring_manager_id, skill_vec VECTOR(8), opened_at; candidates (key candidate_id) with full_name, email, headline, years_experience (long), resume_text, resume_vec VECTOR(8); applications (key application_id) with job_id, candidate_id, stage, source, applied_at, match_score (double), shortlisted (boolean), screen_result.

DODIL MCP tools called
data_bucket_createdata_table_create
Agent

Created bucket blog-hr with workers (PK worker_id), positions (PK position_id), jobs (PK job_id, skill_vec VECTOR(8)), candidates (PK candidate_id, resume_vec VECTOR(8)), and applications (PK application_id). Upserts are idempotent.

Now load the HR core — nine workers in one Engineering org, each holding a position and pointing at a manager (the CTO's manager_id is empty). This is the same shape your Workday/BambooHR export has.

You

Upsert nine workers into blog-hr: w1 Ada Lovelace CTO (no manager, position p1, L8, London); w2 Alan Turing VP Engineering (manager w1, p2, L7); w3 Grace Hopper Engineering Manager Platform (manager w2, p3, L6); w4 Katherine Johnson Engineering Manager Data (manager w2, p4, L6, Manchester); w5 Linus Torvalds Senior Backend Engineer (manager w3, p5, L5); w6 Margaret Hamilton Backend Engineer (manager w5, p6, L4); w7 Barbara Liskov Data Engineer (manager w4, p7, L5, Manchester); w8 Dennis Ritchie Data Engineer (manager w4, p8, L4, Manchester); w9 Sophie Wilson Backend Engineer (manager w3, p9, L4). All Engineering, active.

DODIL MCP tools called
data_table_upsert
Agent

Upserted 9 workers (wal_written: true). Each carries a position_id and a manager_id; the CTO's manager_id is empty.

A manager_id self-join already answers the simple "who reports to whom" — the same query your HRIS runs:

You

In blog-hr, show each worker with their title, level, and their manager's name.

DODIL MCP tools called
data_table_query
Agent

9 rows — Ada Lovelace (CTO, no manager), Alan Turing → Ada, Grace Hopper → Alan, Katherine Johnson → Alan, Linus Torvalds → Grace, Margaret Hamilton → Linus, Sophie Wilson → Grace, Barbara Liskov → Katherine, Dennis Ritchie → Katherine.

A self-join is fine one level deep. But "everyone under Alan, however deep" or "Margaret's full chain to the CTO" are traversals — and that's the next pillar.

Step 2 — The org chart as a graph (the flagship)

This is the question a manager_id column answers badly. DataK3 builds a graph from table rows: a node table with an INTEGER key and an edge table with src/dst. The reporting line is one directed edge per worker — worker → manager — so paths up the tree are the reporting chain and paths down are the org beneath a manager.

Graph v1 keys are integers, so we project the workers into an org_node table (id 1..9, keyed off the numeric part of worker_id) and derive org_edge straight from workers.manager_id. No manual data entry — the graph is a view of the HR core you just loaded.

You

In blog-hr, build the org chart graph. Create org_node (key id long, plus worker_id, full_name, title, department, level) and org_edge (src long, dst long). Populate org_node from workers (id = the number in worker_id). Populate org_edge from workers where manager_id is set (src = worker's id, dst = manager's id). Then CREATE GRAPH orgchart over org_node (KEY id) and org_edge (SRC src DST dst).

DODIL MCP tools called
data_table_createdata_pg
Agent

Built orgchart: org_node (9 rows) + org_edge (8 edges, worker→manager) + CREATE GRAPH orgchart. Node keys are integers; the edge points from each worker up to their manager.

Traversal 1 — everyone under a manager. The VP of Engineering is w2 (id 2). "Who is in his org, however deep?" is a k-hop out over incoming edges (reports point up, so his whole org points at him). graph_khop(graph, start, max_hops, direction) returns node keys + hop distance; JOIN org_node to hydrate names — one statement, no recursive CTE.

You

In blog-hr graph orgchart, list everyone under the VP of Engineering (node id 2), however deep, with their hop distance and titles.

DODIL MCP tools called
data_pg
Agent

7 people. hop 1: Grace Hopper (EM Platform), Katherine Johnson (EM Data). hop 2: Linus Torvalds (Sr Backend), Sophie Wilson (Backend), Barbara Liskov (Data Eng), Dennis Ritchie (Data Eng). hop 3: Margaret Hamilton (Backend).

The same traversal in Cypher over Bolt — anchor on the manager, follow reports inward (the subset returns node keys, so hydrate in SQL):

You

In blog-hr graph orgchart, run Cypher: everyone whose reporting chain reaches node 2, within 10 hops.

DODIL MCP tools called
data_bolt
Agent

7 nodes with hop_distance: {4,3} at hop 1; {8,7,5,9} at hop 2; {6} at hop 3 — the VP's whole org.

Traversal 2 — the reporting chain to the top. Margaret Hamilton is w6 (id 6). Her line of management up to the CTO (id 1) is a shortest path out along the reports-to edges. graph_shortest_path(graph, from, to, direction) returns the ordered step + node; JOIN to name each link.

You

In blog-hr graph orgchart, show Margaret Hamilton's (node 6) reporting chain up to the CTO (node 1), in order.

DODIL MCP tools called
data_pg
Agent

5 steps: 0 Margaret Hamilton (Backend Engineer) → 1 Linus Torvalds (Senior Backend Engineer) → 2 Grace Hopper (EM, Platform) → 3 Alan Turing (VP Engineering) → 4 Ada Lovelace (CTO).

Traversal 3 — span of control. How many direct reports does each manager carry? That's the out-degree of every node that is a dst — a one-hop rollup over the edge table, joined to names.

You

In blog-hr, show each manager and their number of direct reports (span of control), widest first.

DODIL MCP tools called
data_pg
Agent

Alan Turing 2, Grace Hopper 2, Katherine Johnson 2, Ada Lovelace 1, Linus Torvalds 1. Flag anyone over your span threshold.

That's the whole org-chart product — reorg previews, succession lines, span-of-control audits — as three queries over rows you already had. No Neo4j, no nightly sync.

Step 3 — Résumé search as a VECTOR column

Now the recruiting side. Open two reqs and load a handful of candidates. The req's skill_vec and each candidate's resume_vec are 8-dim skill vectors over the axes [backend, distributed_systems, devops_k8s, data_eng, python, go, frontend, ml] — a compact, legible stand-in for a résumé embedding, so the match is easy to read. (In production you fill these from the text: dodil ignite models embed arctic-embed-m-v2 --input "<resume_text>" returns a 768-d vector — declare the column VECTOR(768) and store that literal. The queries below are identical; only the dimension changes.)

You

In blog-hr, upsert two open jobs: j1 Senior Backend Engineer (London, L5, hiring manager w3, skill_vec [0.90,0.90,0.70,0.10,0.80,0.70,0.00,0.10]); j2 Data Engineer (Manchester, L5, hiring manager w4, skill_vec [0.20,0.30,0.30,0.95,0.80,0.10,0.00,0.10]). Then upsert five candidates with resume_text and an 8-dim resume_vec: c1 Grace Chen (backend/Go/distributed/k8s), c2 Omar Farah (data eng), c3 Priya Nair (frontend), c4 Sam Okoye (backend Python), c5 Lena Vogel (ML).

DODIL MCP tools called
data_table_upsert
Agent

Upserted 2 open jobs and 5 candidates (wal_written: true). j1/j2 carry a target skill_vec; each candidate carries a resume_vec on the same 8 axes.

Now the recruiter question: for this open req, who fits best? KNN the candidates against the req's target vector. data vsearch ranks by cosine distance (lower = closer):

You

In blog-hr, KNN-rank the candidates table on resume_vec against the Senior Backend req's target vector [0.90,0.90,0.70,0.10,0.80,0.70,0.00,0.10], cosine, top 5.

DODIL MCP tools called
data_vsearch
Agent

c1 Grace Chen 0.002 · c4 Sam Okoye 0.055 · c2 Omar Farah 0.375 · c5 Lena Vogel 0.466 · c3 Priya Nair 0.858 — the two backend engineers rank first, the frontend candidate last.

Even better: rank straight against a stored req, so a recruiter never pastes a vector. pgvector's <=> runs in ordinary SQL — cross-join the one open req to the candidate pool and order by distance:

You

In blog-hr, rank all candidates for job j1 by resume similarity to j1.skill_vec, closest first, showing the cosine distance.

DODIL MCP tools called
data_table_query
Agent

c1 Grace Chen 0.002 · c4 Sam Okoye 0.055 · c2 Omar Farah 0.375 · c5 Lena Vogel 0.466 · c3 Priya Nair 0.858. The req's own vector drives the ranking — one statement, one bucket.

Step 4 — The screening engine (Ignite)

Candidates apply through your careers page. Each application lands as an applications row (stage: applied). An Ignite app — request-invoked, scale-to-zero — screens each one: it looks up the candidate's resume_vec and the job's skill_vec, computes a match_score = 1 − cosine_distance, sets a shortlisted flag against a threshold, and writes both back into the same applications row. The recruiter opens the req to a ranked, flagged shortlist.

An Ignite app is a separate workload, so it needs its own service account to call DataK3 and Models (client-credentials → bearer token), injected as runtime env. Grant it least privilege: k3.editor to write applications, and the Ignite developer role.

Load the funnel with five applications, then screen them:

You

In blog-hr, upsert five applications (stage applied): a1 c1→j1 (referral), a2 c4→j1 (linkedin), a3 c3→j1 (careers-page), a4 c5→j1 (careers-page), a5 c2→j2 (referral).

DODIL MCP tools called
data_table_upsert
Agent

Upserted 5 applications keyed on application_id (wal_written: true). Four candidates on the backend req, one on the data req.

The real handler. It mints its SA token, runs the vector match as SQL (the DB computes <=>), then upserts the score and flag back onto the application. This is the whole engine — under 50 lines:

# engine/handler.py — Ignite screening app. Invoked with {"application_id": "..."}.
 
 
BUCKET   = "blog-hr"
THRESHOLD = 0.70                                  # shortlist cutoff on match_score
TOKEN_URL = "https://id.dodil.io/realms/dodil/protocol/openid-connect/token"
PG_DSN    = "postgresql://token:{tok}@pg.uk-lon-1.dodil.io:5432/" + BUCKET + "?sslmode=require"
 
def _token() -> str:                              # client_credentials -> bearer
    r = requests.post(TOKEN_URL, data={
        "grant_type": "client_credentials",
        "client_id": os.environ["DODIL_SERVICE_ACCOUNT_ID"],
        "client_secret": os.environ["DODIL_SERVICE_ACCOUNT_SECRET"],
    }, timeout=30)
    r.raise_for_status()
    return r.json()["access_token"]
 
def handler(payload, ctx):
    app_id = payload["application_id"]
    tok = _token()
 
    # 1) vector match: the DB ranks the résumé against the req it applied to
    with psycopg.connect(PG_DSN.format(tok=tok)) as conn:
        row = conn.execute("""
            SELECT a.job_id, a.candidate_id,
                   1 - (c.resume_vec <=> j.skill_vec) AS match_score
            FROM applications a
            JOIN candidates c ON c.candidate_id = a.candidate_id
            JOIN jobs       j ON j.job_id       = a.job_id
            WHERE a.application_id = %s
        """, (app_id,)).fetchone()
    job_id, cand_id, score = row[0], row[1], float(row[2])
    shortlisted = score >= THRESHOLD
 
    # 2) write the verdict BACK onto the same application row (K3 HTTP merge; rows are JSON strings)
    merge = {"rows": [json.dumps({
        "application_id": app_id, "match_score": round(score, 3),
        "shortlisted": shortlisted, "stage": "screened",
    })]}
    requests.post(f"https://api.dodil.io/k3/{BUCKET}/tables/applications/merge",
                  headers={"Authorization": f"Bearer {tok}"}, json=merge, timeout=30).raise_for_status()
 
    return {"application_id": app_id, "job_id": job_id, "candidate_id": cand_id,
            "match_score": round(score, 3), "shortlisted": shortlisted}
You

Create a service account for the screening engine, grant it k3.editor and the Ignite developer role, deploy ./engine as hr-screening with the SA creds as runtime env, and invoke it for application a1.

DODIL MCP tools called
auth_service-account_createauth_service-account_grant-roleignite_app_deployignite_invoke
Agent

Deployed hr-screening (scale-to-zero). Invoke a1 → {match_score: 0.998, shortlisted: true}; the row is now stage=screened with the score written back.

Screening the whole backend req writes these scores back (match_score, shortlisted) — exactly what the handler's merge produces, run across every application:

You

In blog-hr, compute the match_score for every application (1 - cosine distance between the candidate resume_vec and the job skill_vec) and write match_score + shortlisted (threshold 0.70) + stage='screened' back onto each application row.

DODIL MCP tools called
data_table_upsertdata_table_query
Agent

Backend req j1: Grace Chen 0.998 ✓, Sam Okoye 0.945 ✓, Lena Vogel 0.534 ✗, Priya Nair 0.142 ✗. Data req j2: Omar Farah 0.997 ✓. Two shortlisted for backend, one for data — written back to applications.

Step 5 — The knock-out gate (Models)

Similarity ranks; it doesn't decide. A résumé can look close in vector space and still miss a hard requirement — the 5-year minimum, a specific language, a work-authorization or Kubernetes must-have. That's a reasoning call, and it's where recruiter hours actually go: reading every shortlisted résumé against the req's non-negotiables. So we run it once per shortlisted application on a Models call and write the verdict to screen_result.

Lead with the money: at, say, 200 applicants a req and 3 minutes of a recruiter's read each, that's 10 hours per req of first-pass screening. The gate does the first pass for a fraction of a cent per candidate — and only on the shortlist, so you never pay to reason about the frontend developer who applied to a backend role.

You

Screen candidate Grace Chen against the Senior Backend Engineer req's hard requirements (5+ years backend, Python or Go, distributed systems, Kubernetes) on a low-cost model. Return only JSON: decision pass|fail, reasons (<=18 words). Fail only if a hard requirement is clearly missing.

DODIL MCP tools called
ignite_models_chat
Agent

{"decision":"pass","reasons":"Meets all hard requirements: 8 years, Python/Go, distributed systems, Kubernetes."}

The gate discriminates — feed it the frontend candidate who slipped into the backend req and it knocks them out with a reason:

You

Screen Priya Nair (5 years frontend: React, TypeScript, Next.js, some Node) against the same Senior Backend req and hard requirements.

DODIL MCP tools called
ignite_models_chat
Agent

{"decision":"fail","reasons":"Frontend only. Missing backend experience, Python/Go, distributed systems, Kubernetes."}

The engine writes each verdict to screen_result on the application (the same merge as Step 4). Across the shortlist, all three pass — the two backend engineers on j1 and the data engineer on j2:

You

In blog-hr, write the Models knock-out verdict to screen_result for each shortlisted application (a1 pass, a2 pass, a5 pass), then show the final funnel: job, candidate, match_score, shortlisted, screen_result.

DODIL MCP tools called
data_table_upsertdata_table_query
Agent

Final funnel — j1: Grace Chen 0.998/true/pass, Sam Okoye 0.945/true/pass, Lena Vogel 0.534/false/(skipped), Priya Nair 0.142/false/(skipped); j2: Omar Farah 0.997/true/pass. The gate ran only on the shortlist.

Query it — one bucket, three pillars, drop-in clients

The same rows answer by content (SQL), by relationship (graph), and by meaning (vector) — no ETL, no second copy. Headcount by department and open reqs are plain SQL; the org and the shortlist you saw above. And any Postgres/pgvector or Bolt/Neo4j client points straight at the bucket — data connect prints the endpoints (DB name = bucket, credential = your login token).

You

In blog-hr, show headcount by department and the open requisitions, then print the pg/bolt/grpc endpoints so I can point psql and cypher-shell at the bucket.

DODIL MCP tools called
data_sqldata_connect
Agent

Headcount: Engineering 9. Open reqs: p10 Senior Backend Engineer (London, L5), p11 Data Engineer (Manchester, L5). Endpoints: pg pg.uk-lon-1.dodil.io:5432/blog-hr · bolt bolt+s://bolt.uk-lon-1.dodil.io:7687 · grpc table-rpc.uk-lon-1.dodil.io:443.

DataK3 vs. the multi-system stack

The jobThe usual systemOn DataK3
Workers, positions, funnelWorkday / BambooHR + Greenhouse (per-employee + per-seat)Merge-keyed SQL tables in the bucket
Org chart traversalsNeo4j (synced nightly) or recursive CTEsgraph_khop / graph_shortest_path over the same worker rows
Résumé / skills matchPinecone + embed-and-sync glueA VECTOR column + pgvector <=>
Screening + knock-outA bespoke service + a model vendorIgnite app + ignite models chat, verdict written back
Keeping them in syncETL between all of the aboveNone — one copy of the rows

One bucket, one bill, one auth context — and a new hire's manager_id is in the org graph and their seat in positions the instant it's written, with nothing to sync.

Test

Runnable end-to-end. Every command below was run live against DataK3 on blog-hr (2026-09-01); the ## Query it outputs above are the real results. The Ignite screening engine's handler.py is complete and runnable and its deploy/invoke commands are shown as code — the vector match and the write-back it performs were validated directly (Steps 3–4); the two ignite models chat knock-out calls (pass and fail) were run live.

export BUCKET=blog-hr
 
# SQL core — the funnel is loaded
dodil data table query -b "$BUCKET" "SELECT count(*) AS n FROM applications"          # expect: n = 5
 
# Graph — everyone under the VP (node 2) is the 7-person org
dodil data pg -b "$BUCKET" "SELECT count(*) AS n FROM graph_khop('orgchart', 2, 10, 'in')"   # expect: n = 7
 
# Graph — Margaret's reporting chain to the CTO is 5 people deep
dodil data pg -b "$BUCKET" "SELECT count(*) AS n FROM graph_shortest_path('orgchart', 6, 1, 'out')"  # expect: n = 5
 
# Vector — the closest résumé to the backend req is Grace Chen (c1)
dodil data table query -b "$BUCKET" \
  "SELECT c.candidate_id FROM candidates c CROSS JOIN jobs j WHERE j.job_id='j1' ORDER BY c.resume_vec <=> j.skill_vec LIMIT 1"   # expect: c1
 
# Funnel — shortlisted + passed applications
dodil data table query -b "$BUCKET" \
  "SELECT count(*) AS n FROM applications WHERE shortlisted = true AND screen_result='pass'"   # expect: n = 3
 
# Models gate — the knock-out call returns JSON {decision, reasons}
dodil ignite models chat kimi-k2.6 \
  --system 'Return ONLY JSON {"decision":"pass|fail","reasons":"<=18 words"}.' \
  --message 'JOB hard reqs: 5+ years backend, Python/Go, distributed systems, Kubernetes. CANDIDATE: 8 years Python/Go distributed systems, Kubernetes.'   # expect: decision=pass

One-shot

Build an HRIS + ATS on ONE DataK3 bucket. Confirm each step.
 
1. Create bucket blog-hr, then merge-keyed tables:
   - workers (key worker_id): full_name, email, title, position_id, manager_id, department, location, level, hire_date, status.
   - positions (key position_id): title, department, level, location, job_family, filled (boolean), worker_id.
   - jobs (key job_id): title, department, location, level, description, status, hiring_manager_id, skill_vec VECTOR(8), opened_at.
   - candidates (key candidate_id): full_name, email, headline, years_experience (long), resume_text, resume_vec VECTOR(8).
   - applications (key application_id): job_id, candidate_id, stage, source, applied_at, match_score (double), shortlisted (boolean), screen_result.
2. Load a 9-person Engineering org (a CTO, a VP, two EMs, five ICs), each with a position_id and a manager_id (CTO's manager empty).
3. Build the org graph: org_node (INTEGER id from worker_id) + org_edge (src=worker, dst=manager) derived from workers; CREATE GRAPH orgchart. Then: everyone under the VP (graph_khop 'in'), a worker's reporting chain to the CTO (graph_shortest_path 'out'), and each manager's span of control.
4. Open two reqs (backend, data) with skill_vec targets; load five candidates with resume_vec on axes [backend, distributed, devops_k8s, data_eng, python, go, frontend, ml]. Rank candidates for a req with `resume_vec <=> skill_vec`.
5. Load five applications; write a screening engine (Ignite handler.py, its own service account) that computes match_score = 1 - cosine distance and writes match_score + shortlisted (threshold 0.70) back onto each application.
6. For each shortlisted application, call `ignite models chat` with the req's hard requirements and write the pass/fail verdict to screen_result.
7. Show the final funnel (job, candidate, match_score, shortlisted, screen_result) and print the pg/bolt endpoints.
 
Vectors are VECTOR(dim) columns (no store); writes need a PRIMARY KEY; reads are read-your-writes.
In production, fill resume_vec/skill_vec with `ignite models embed arctic-embed-m-v2` (VECTOR(768)).

Ship it — make hr-screening a public endpoint

The hr-screening app above is shown as code. Ship it the last mile so users hit a real URL, not a snippet — the managed runtime compiles your handler.py and returns a public FQDN:

You

Deploy the hr-screening app publicly with no auth and give me its URL.

DODIL MCP tools called
ignite_app_deployignite_app_get
Agent

Deployed hr-screening (deployment_state: deployed). Its public FQDN on ignite.dodil.cloud is callable with no token — or call it with dodil ignite invoke hr-screening.

That's the quick path (managed compile). The full supply chain — DODIL git → CI checks → a scanned image in the DODIL registry → versioning and one-command rollback — is its own tutorial: Ship a DODIL App.

Conclusion

You now have an HRIS + ATS on one DataK3 bucket — the HR core and recruiting funnel as merge-keyed SQL, the org chart as a graph you traverse three ways, résumés as a vector column you rank by meaning, an Ignite engine that screens each application, and a Models gate that runs the knock-out. No Postgres + Neo4j + Pinecone + a screening service to license and sync — one copy of the rows, three pillars, one bill.

You can do this today: create a free account, install the CLI, connect your agent over MCP, and stand up the core this afternoon.

Next steps: