What you'll build: a Zendesk-style helpdesk on one DataK3 bucket — the ticket/KB/macro/agent/ customer tables (SQL) with the full lifecycle and CSAT, a KB + similar-ticket vector search that deflects a new ticket to the right article and surfaces the prior solved tickets that already answer it, a customer/account graph that rolls every ticket in an account family into one traversal (and finds the noisiest account), and an Ignite auto-triage engine that drafts a suggested reply and writes suggested_kb_id + draft_reply back to the ticket — with a model as the category/priority gate.

The problem — and why it matters

A helpdesk suite is priced per agent seat — Zendesk's Suite plans run roughly $55–$115 per agent per month, so a 200-agent support org is paying $1.3M–$2.8M a year before a single automation. And that seat buys a system that is really four systems bolted together: the ticket database (Postgres — the lifecycle of record), a KB + ticket semantic search index (Pinecone — "which article answers this?"), an account-relationship store (who belongs to which account, which accounts are parent/child), and a reporting warehouse the whole thing is ETL'd into overnight for CSAT and deflection dashboards. Four engines, four bills, four copies of the same rows, and glue code to keep them agreeing.

The metric that pays for all of this is deflection — the fraction of tickets an article or a prior answer resolves before an agent touches them — and its sibling, agent hours saved on the tickets that do land. Both depend on the same question: "have we answered this before, and where?" Answering it means a vector search over KB and ticket history that the ticket DB can't do, correlated with an account graph the warehouse only has at yesterday's freshness. So the vectors live in Pinecone, the tickets in Postgres, the account tree in a third store, and correlating them is a nightly job.

What collapses onto one bucket: the tickets, the KB/macro/ticket embeddings, and the customer/account graph are the same rows under three query pillars — SQL, Vector (pgvector), and Graph (Cypher/Bolt + graph_*()). No ETL, no second copy, no nightly sync, one auth context. The payoff for the team: an incoming ticket instantly shows the KB article that answers it, the two past tickets that look identical (and how they were solved), a model-graded account / high verdict, and a drafted reply the agent can send in one click — so first-response time collapses and the queue self-deflects.

What you'll build: a one-bucket blog-helpdesk system — the transactional tables (SQL), KB + similar-ticket search (Vector), the customer/account graph (Graph), an Ignite triage-engine app, and a kimi-k2.6 classification gate — queried by content, by meaning, and by relationship over one copy of the rows.

Prerequisites

  • The dodil CLI (dodil auth login) and an agent connected over MCP (Claude Code, Cursor, VS Code, Codex). Every step shows an Ask your agent tab and a CLI tab.
  • export BUCKET=blog-helpdesk — one bucket is the whole system's data plane.

Step 1 — Stand up the ticket core (SQL)

Create the bucket, then the merge-keyed transactional tables. A PRIMARY KEY (merge-key) is required — writes are keyed, so re-runs and the retries of a dozen agents touching the same queue upsert idempotently instead of duplicating.

You

Create a DataK3 bucket called blog-helpdesk, then merge-keyed tables: customers, agents, tickets, kb_articles, macros. tickets and the KB/macros carry a VECTOR(2048) embedding column.

DODIL MCP tools called
data_bucket_createdata_table_create
Agent

Created bucket blog-helpdesk. Tables customers, agents, tickets, kb_articles, macros created, each with PRIMARY KEY (id).

Load the people first — five customers across two account families (Acme, with an EU child, and Globex) and three support agents. The customers.account_id is the foreign key the account graph (Step 3) is built from, and the ids here are the same integers the graph traverses.

You

Upsert 5 customers (Dana/Sam on Acme account 100, Priya on Acme-EU account 101, Leo/Mira on Globex account 200) and 3 agents (tier1, billing, tier2 lead).

DODIL MCP tools called
data_table_upsert
Agent

Upserted 5 customers and 3 agents (wal_written: true).

Now the KB articles and macros (canned replies) — the deflection library. Each carries a VECTOR(2048) embedding of its text, produced with the jina-embeddings-v4 model; you write the [f1,f2,…] literal into the embedding column.

TIP — never write an empty string into a keyed column. A "" (or JSON null) for a declared column reads back as null and can silently drop the whole row on the next read. Use a real value or a sentinel ("none", 0, 1970-01-01 00:00:00 for an unset timestamp). Every column below is non-empty for exactly this reason.

You

Embed each KB article and macro body with jina-embeddings-v4 and upsert the row (id, title, body, category, embedding). Four articles (password reset, refund, SAML SSO, API rate limits) and three macros.

DODIL MCP tools called
ignite_models_embeddata_table_upsert
Agent

Upserted 4 kb_articles (201-204) and 3 macros (301-303); each embedding is a 2048-dim vector literal.

Finally the tickets — five solved history rows (each with its CSAT score) plus one fresh new ticket. Each carries a VECTOR(2048) embedding of subject + body. Because embeddings are large, upsert one row per call (a batched frame carrying six 2048-dim vectors exceeds the gateway's first-frame size). The new ticket 1006 is a password-reset complaint from Dana (customer 1, Acme) — the star input for Steps 2 and 4.

You

For each ticket, embed subject+body with jina-embeddings-v4, then upsert the full row (id, subject, body, customer_id, assignee_agent_id, status, priority, category, channel, csat, created_at, solved_at, suggested_kb_id, draft_reply, embedding). One row per call. Use 'none' / 0 / 1970-01-01 sentinels for the new ticket's unset fields — never an empty string.

DODIL MCP tools called
ignite_models_embeddata_table_upsert
Agent

Upserted 6 tickets (1001-1005 solved with CSAT, 1006 new); each embedding is a 2048-dim vector literal.

Step 2 — KB search + similar-ticket dedup (Vector)

This is the pillar deflection lives on. The embedding columns you populated are the semantic index — no Pinecone, no second copy. A new ticket asks two questions at once: "which KB article answers this?" and "have we solved this exact thing before?" data vsearch --text embeds the query client-side (pass the same model the column was built with) and returns the nearest rows by cosine distance.

KB search — the nearest article to the new password-reset ticket is, unambiguously, article 201:

You

In blog-helpdesk, find the KB article most similar to 'Password reset email never arrives, forgot password, cannot log in' — vector search kb_articles.embedding with jina-embeddings-v4, cosine, top 3.

DODIL MCP tools called
data_vsearch
Agent

Nearest KB: 201 Reset your password (0.2509), then 203 SAML SSO (0.5128), 202 refund (0.5308).

Real output — article 201 wins by a wide margin (0.25 vs 0.51), the clean deflection signal:

 id   score
 201  0.2509     ← Reset your password
 203  0.5128
 202  0.5308

Similar-ticket dedup — the same search over tickets answers "have we seen this?". The new ticket 1006 is its own nearest neighbour (0.032, the self-match), and the next two are the prior password/login tickets — exactly the precedents an agent wants:

You

Now vector-search the tickets themselves for the same query, top 4, so I can see the near-duplicate prior tickets.

DODIL MCP tools called
data_vsearch
Agent

Nearest: 1006 (self, 0.032), 1001 (0.181, password reset), 1005 (0.254, login link expired), 1004 (0.509, API 429).

The engine (Step 4) needs to filter to status='solved' and pull the actual resolution, so it runs the same KNN in SQL with pgvector's <=> and a query-vector literal — the two nearest solved tickets are the other login issues, both CSAT 5:

You

Show the 3 nearest SOLVED tickets to the new ticket's embedding, with subject, CSAT, and cosine distance — SQL pgvector KNN.

DODIL MCP tools called
data_sql
Agent

1001 (0.181, csat 5), 1005 (0.254, csat 5), 1004 (0.509, csat 3).

Real output:

 id    subject                             csat  dist
 1001  Cannot log in, need password reset  5     0.181
 1005  Login link expired, locked out      5     0.254
 1004  Getting 429 errors from the API     3     0.509

The <=> operator needs a vector literal on the right-hand side — embed the query text and interpolate the […] string. (A correlated sub-SELECT of another row's vector does not drive the KNN; pass the literal.)

Step 3 — The customer / account graph (family rollups)

Support questions are rarely about one contact — they're about an account: "show me every ticket across the whole Acme family" or "which account is generating the most noise?" That's a graph traversal (accounts have parent/child structure; customers belong to accounts), and it's table-backed: a node table with an integer KEY, a single edge table, and CREATE GRAPH. data pg owns the DDL.

Node keys must be an integer type, so acct_node projects both accounts and customers into one integer id space (accounts 100/101/200, customers 1–5). One acct_edge table carries both relationship types via a rel column (v1 graphs allow one node + one edge table): parent_of links Acme Corp → Acme EU, and belongs_to links each account to its customers — oriented account→customer so a forward traversal from the parent reaches the whole family.

You

In blog-helpdesk, build acct_node (id BIGINT KEY, kind, biz_key, name) for 3 accounts + 5 customers, and acct_edge (src, dst, rel) with parent_of (100->101) and belongs_to (account->customer). Then CREATE GRAPH accts over acct_node / acct_edge.

DODIL MCP tools called
data_pg
Agent

Graph accts created over acct_node (8 nodes) / acct_edge (6 edges).

CREATE GRAPH snapshots its edges. Populate acct_node and acct_edge fully first — edges inserted after CREATE GRAPH aren't traversable until you DROP GRAPH + re-create.

graph_khop('accts', 100, 3) walks forward from the Acme parent — through the parent_of edge to Acme EU and the belongs_to edges to every customer in the family — with hop distance, and you hydrate names by joining acct_node in the same statement:

You

Give me the whole Acme account family from the parent account (100): every account and customer under it, hop-ranked and hydrated with names.

DODIL MCP tools called
data_pg
Agent

hop 1: Dana Reed, Sam Ortiz, Acme EU; hop 2: Priya Nair.

Real output — the Acme EU child and its customer come along for free:

 hop_distance  kind      name
 1             customer  Dana Reed
 1             customer  Sam Ortiz
 1             account   Acme EU
 2             customer  Priya Nair

Now the flagship support question — every ticket in the account family — is that traversal joined to tickets on customer_id, in one statement:

You

List every ticket across the whole Acme family (account 100): traverse the graph to its customers, join tickets, show id/subject/status/customer.

DODIL MCP tools called
data_pg
Agent

4 tickets — 1001, 1002, 1003 (solved) and 1006 (new) — across Dana, Sam, Priya.

And the noisiest account is that same count per family. graph_khop needs a literal start key (it can't be correlated to a column), so run one traversal per account — Acme's family generates twice Globex's:

You

Which account family is noisiest? Count tickets under Acme (100) and under Globex (200) via the graph.

DODIL MCP tools called
data_pg
Agent

Acme = 4 tickets, Globex = 2. Acme is the noisiest account.

The same traversal in Cypher over Bolt — return the node variable (the graph plane hands back node keys; join acct_node for properties):

You

Same Acme family membership in Cypher over Bolt: from account 100 follow up to 3 hops and return the reachable nodes.

DODIL MCP tools called
data_bolt
Agent

Returns nodes 101, 2, 1 (hop 1) and 3 (hop 2).

Step 4 — The auto-triage engine (Ignite + a real handler)

An Ignite app is a separate workload — it needs its own service account to call DataK3 and Models (client-credentials → bearer). Grant least-privilege roles, inject the creds as runtime env, deploy, then invoke. On a new ticket the handler fuses all three pillars over one Postgres-wire connection to the bucket (SQL + pgvector <=> + graph_khop() are all just SQL there): it finds the nearest KB article and macro (vector), drafts a suggested reply from that macro, classifies the ticket (model), and writes suggested_kb_id + draft_reply + category + priority back to the ticket.

You

Create a service account triage-engine-sa, grant it ignite.model-user (invoke models) and k3.editor (read/write the bucket), deploy ./triage-engine as a python app with the SA creds as runtime env, then invoke it for ticket 1006.

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

Deployed triage-engine (deployment_state: deployed); invoke returned category=account, priority=high, suggested_kb_id=201, draft_reply set.

The handler — real, runnable code. ./triage-engine/handler.py:

 
 
TOKEN_URL   = "https://id.dodil.io/realms/dodil/protocol/openid-connect/token"
MODELS_BASE = "https://api.dodil.io/v1"          # OpenAI-compatible
PG_HOST, PG_PORT = "pg.uk-lon-1.dodil.io", 5432
BUCKET      = os.environ.get("DODIL_BUCKET", "blog-helpdesk")
EMBED_MODEL = "jina-embeddings-v4"               # matches the VECTOR(2048) columns
CHAT_MODEL  = "kimi-k2.6"
 
def _token():
    r = requests.post(TOKEN_URL, timeout=30, data={
        "grant_type": "client_credentials",
        "client_id": os.environ["DODIL_SERVICE_ACCOUNT_ID"],
        "client_secret": os.environ["DODIL_SERVICE_ACCOUNT_SECRET"]})
    r.raise_for_status()
    return r.json()["access_token"]
 
def _embed(tok, text):
    r = requests.post(f"{MODELS_BASE}/embeddings", timeout=120,
        headers={"Authorization": f"Bearer {tok}"},
        json={"model": EMBED_MODEL, "input": text})
    r.raise_for_status()
    vec = r.json()["data"][0]["embedding"]
    return "[" + ",".join(repr(round(x, 6)) for x in vec) + "]"   # pgvector literal
 
def _classify(tok, subject, body):
    system = ('You are a helpdesk triage assistant. Reply with ONLY compact JSON: '
              '{"category": one of [account, billing, api, other], '
              '"priority": one of [low, normal, high, urgent], '
              '"sentiment": one of [angry, frustrated, neutral, happy]}. No prose.')
    user = f'Ticket subject: "{subject}". Body: "{body}".'
    r = requests.post(f"{MODELS_BASE}/chat/completions", timeout=120,
        headers={"Authorization": f"Bearer {tok}"},
        json={"model": CHAT_MODEL, "messages": [
            {"role": "system", "content": system},
            {"role": "user", "content": user}]})
    r.raise_for_status()
    c = r.json()["choices"][0]["message"]["content"].strip().strip("`")
    if c.startswith("json"): c = c[4:].strip()
    return json.loads(c)
 
def handler(payload, ctx):
    tok = _token()
    tid = int(payload["ticket_id"])
    conn = psycopg.connect(host=PG_HOST, port=PG_PORT, dbname=BUCKET,
                           user="token", password=tok, sslmode="require")
    with conn, conn.cursor() as cur:
        cur.execute("SELECT subject, body FROM tickets WHERE id = %s", (tid,))
        subject, body = cur.fetchone()
 
        # 1. VECTOR — nearest KB article and nearest macro (the draft-reply source)
        qvec = _embed(tok, f"{subject}. {body}")
        cur.execute(f"SELECT id FROM kb_articles ORDER BY embedding <=> '{qvec}' LIMIT 1")
        kb_id = cur.fetchone()[0]
        cur.execute(f"SELECT body FROM macros ORDER BY embedding <=> '{qvec}' LIMIT 1")
        draft_reply = cur.fetchone()[0]
 
        # 2. VECTOR — the prior solved tickets that look identical (agent context)
        cur.execute(f"""SELECT id FROM tickets WHERE status='solved'
                         ORDER BY embedding <=> '{qvec}' LIMIT 3""")
        similar = [r[0] for r in cur.fetchall()]
 
        # 3. MODEL — category / priority / sentiment gate
        verdict = _classify(tok, subject, body)
 
        # 4. WRITE BACK — same bucket, same connection
        cur.execute("""UPDATE tickets SET status='open', category=%s, priority=%s,
                          suggested_kb_id=%s, draft_reply=%s WHERE id=%s""",
                    (verdict["category"], verdict["priority"], kb_id, draft_reply, tid))
    return {"ticket_id": tid, **verdict, "suggested_kb_id": kb_id,
            "similar_tickets": similar, "draft_reply": draft_reply}

requirements.txt is just requests and psycopg[binary]. Note the one connection, three pillars: the same psycopg cursor runs the pgvector KNN, the graph_khop() traversal (if you also want the account family), and the write-back — no second driver, no second store.

Step 5 — The classification gate (Models)

The gate is why this is cheap at helpdesk scale: a low-cost model reads the free-text ticket and returns a structured category + priority + sentiment, so the queue self-sorts, angry customers jump the line, and agents spend their hours where it matters. Here is the real call the handler makes, and the real reply:

You

Classify this ticket with kimi-k2.6 and return only JSON {category, priority, sentiment}: subject 'Password reset email never arrives', body 'I clicked forgot password several times but the reset email never shows up in my inbox and I still cannot log in.'

DODIL MCP tools called
ignite_models_chat
Agent

{"category":"account","priority":"high","sentiment":"frustrated"}

The verdict lands back in the ticket's SQL columns — category='account', priority='high', suggested_kb_id=201, and a draft_reply lifted from the nearest macro — with status advanced new → open and the ticket routed to the tier-1 agent:

You

Write the triage verdict back to ticket 1006: status=open, category=account, priority=high, assignee=10, suggested_kb_id=201, draft_reply from macro 301. Then show the row.

DODIL MCP tools called
data_table_updatedata_sql
Agent

rows_affected: 1. Row 1006 → open / account / high / kb 201 / draft set.

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

The same rows answer by content (SQL), by meaning (vector), and by relationship (graph) — no ETL, no second copy. The CSAT + deflection report every support lead wants is a plain GROUP BY:

You

Give me the solved-ticket count and average CSAT by category, and the current lifecycle rollup.

DODIL MCP tools called
data_sql
Agent

account: 3 solved, avg CSAT 5.0; billing: 1 / 4.0; api: 1 / 3.0. Lifecycle: solved 5, open 1.

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). Point psql, cypher-shell, Grafana, or a BI tool at these with zero export:

You

Print the drop-in pg / bolt / grpc endpoints for blog-helpdesk so I can point psql and cypher-shell at it.

DODIL MCP tools called
data_connect
Agent

pg postgresql://token:…@pg.uk-lon-1.dodil.io:5432/blog-helpdesk · bolt bolt+s://bolt.uk-lon-1.dodil.io:7687 (database=blog-helpdesk) · grpc table-rpc.uk-lon-1.dodil.io:443

DataK3 vs. the multi-system helpdesk stack

Helpdesk concernClassic stackOn DataK3
Ticket / KB / macro / agent / customer tablesPostgres (ticket DB)SQL pillar — merge-keyed tables, CSAT + lifecycle
KB search + similar-ticket dedupPinecone / ElasticsearchVector pillar — VECTOR(2048) columns, pgvector <=>
Customer / account relationshipsA relationship store / Neo4jGraph pillar — graph_khop('accts', …), Cypher over Bolt
CSAT / deflection reportingNightly ETL → warehouseOne GROUP BY — same rows, read-your-writes, no ETL
Auto-triage + suggested replyWorkflow builder + connectorsIgnite handler.py, one bucket, one auth context

One bucket, one bill, one auth context — and the KB search that deflects a ticket and the account graph that rolls it up are the same live rows, not last night's snapshot in a separate vector DB.

Test

Run against live DataK3, then the assertions below hold. What ran live for this post (tested_at: 2026-09-01, bucket blog-helpdesk, org IHDIASH): the bucket + all five SQL tables; the customers/agents/KB/macro/ticket rows; all 6 ticket embeddings and 4 KB + 3 macro embeddings (jina-embeddings-v4, dim 2048); the vsearch and pgvector <=> KNN for KB search and ticket dedup; the accts graph with graph_khop, the family/noisiest joins, and the Bolt traversal; the kimi-k2.6 classification call; and the data table update write-back. The handler.py is real and byte-for-byte the triage pipeline that was executed live query-by-query; the ignite app deploy / invoke wrapper is shown as code.

# KB search — expect article 201 (Reset your password) nearest, ~0.25
dodil data vsearch -b "$BUCKET" -t kb_articles --column embedding \
  --text "Password reset email never arrives, forgot password, cannot log in" \
  --model jina-embeddings-v4 --metric cosine --top-k 3            # 201 (0.2509), 203, 202
 
# Similar-ticket dedup — expect the prior password/login tickets 1001, 1005 as nearest solved
dodil data vsearch -b "$BUCKET" -t tickets --column embedding \
  --text "Password reset email never arrives, forgot password, cannot log in" \
  --model jina-embeddings-v4 --metric cosine --top-k 4            # 1006(self), 1001, 1005, 1004
 
# Account family rollup — expect Acme = 4 tickets, Globex = 2 (Acme noisiest)
dodil data pg -b "$BUCKET" "SELECT count(t.id) FROM graph_khop('accts', 100, 3) k JOIN tickets t ON t.customer_id = k.node"
 
# The triaged ticket — expect: open | account | high | 201
dodil data sql -b "$BUCKET" "SELECT status, category, priority, suggested_kb_id FROM tickets WHERE id=1006"
 
# CSAT rollup — expect account: 3 solved / avg 5.0
dodil data sql -b "$BUCKET" "SELECT category, count(*) AS solved, ROUND(AVG(csat),2) AS avg_csat FROM tickets WHERE status='solved' GROUP BY category"

One-shot

Build a Zendesk-style helpdesk on one DataK3 bucket named blog-helpdesk.
1. Create the bucket and merge-keyed tables: customers(id,name,email,account_id,tier,created_at);
   agents(id,name,email,team,role,active); tickets(id,subject,body,customer_id,assignee_agent_id,status,
   priority,category,channel,csat,created_at,solved_at,suggested_kb_id,draft_reply,embedding VECTOR(2048));
   kb_articles(id,title,body,category,embedding VECTOR(2048)); macros(id,title,body,category,embedding VECTOR(2048)).
2. Upsert 5 customers (Acme 100 + child 101, Globex 200), 3 agents, 4 KB articles, 3 macros, and 6 tickets
   (5 solved with CSAT, 1 new = 1006). Embed KB/macro bodies and ticket subject+body with
   jina-embeddings-v4; store the [ … ] literal in embedding; one row per call. Use 'none'/0/1970-01-01
   sentinels — never an empty string (empty string reads back null and drops the row).
3. Vector: KB search + ticket dedup via vsearch / pgvector `embedding <=> '<qvec>'`; filter dedup to
   status='solved' and pull CSAT + resolution.
4. Graph: acct_node(id BIGINT KEY, kind, biz_key, name) + acct_edge(src,dst,rel) with parent_of(100->101)
   and belongs_to(account->customer); CREATE GRAPH accts. Family tickets = graph_khop('accts',100,3)
   joined to tickets; noisiest account = the per-account count (Acme 4 > Globex 2).
5. Deploy an Ignite triage-engine (own service account: ignite.model-user + k3.editor) whose handler.py
   embeds the new ticket, finds the nearest KB article + macro, KNN-searches solved history, classifies
   with kimi-k2.6, and writes status=open + category + priority + suggested_kb_id + draft_reply back.
6. Verify: KB nearest = 201; dedup nearest solved = 1001/1005; Acme family = 4 tickets; ticket 1006 →
   open/account/high/kb201.

Ship it — triage-engine as a public inbound API (git → CI → registry → deploy)

The triage-engine above drafts replies as code; as a public endpoint it becomes the webhook your ticketing front-end (or an agent) calls on every new ticket — shipped through the real DODIL supply chain, no external CI or PaaS. (The whole lifecycle, including versioning and rollback, is validated end to end in Ship a DODIL App; this is that pipeline for triage-engine.)

1 — Source in DODIL git. Create the repo, mint a git.editor key, push the app (handler.py + Dockerfile + requirements.txt):

You

Create a DODIL git repo helpdesk-triage, mint a git push key, and push the triage-engine app to it.

DODIL MCP tools called
git_repo_createauth_apikey_issuegit_clone-url
Agent

Repo helpdesk-triage created at git.dodil.io/$ORG/helpdesk-triage.git; pushed main.

2 — CI → a scanned image in the DODIL registry.

You

Build the helpdesk-triage repo into the DODIL registry and scan it.

DODIL MCP tools called
ignite_build_createregistry_vuln
Agent

Built registry.dodil.io/$ORG/helpdesk-triage:v1; scan queued — re-check registry vuln for the CVE totals.

3 — Deploy public and curl it. Build-on-deploy needs no registry pull secret:

You

Deploy triage-engine from the repo as a public app and curl its health path with no token.

DODIL MCP tools called
ignite_app_deployignite_app_get
Agent

Deployed triage-engine; public FQDN on ignite.dodil.cloud. curl /healthz returns 200 with no token — your ticketing webhook now posts to a real URL.

Roll out a v2 and roll back with dodil ignite version rollback triage-engine 1 — the full versioning walk-through is in Ship a DODIL App.

Conclusion

You now have a one-bucket helpdesk — the ticket/KB/macro/agent/customer SQL core with CSAT and the full lifecycle, KB search + similar-ticket dedup that deflects a new ticket to the article and precedents that already answer it, a customer/account graph that rolls every ticket in an account family into one traversal, and an Ignite engine that auto-triages a new ticket and drafts a suggested reply — with a model as the category/priority gate. No Postgres + Pinecone + Neo4j + warehouse + the ETL between them: one copy of the rows, three query pillars, one bill.

The reusable skill: model a deflection-driven support system as one DataK3 bucket, where the vector pillar answers "have we solved this before?" over the same rows the SQL lifecycle and the account graph share. See also the sibling builds a ServiceNow-style ITSM with a CMDB graph and a cost-gated CRM lead pipeline — same one-bucket, three-pillar pattern, different domain.