An ITSM is not one thing — it's a system of record plus half a dozen workflows that all read the same CIs, incidents, and changes. The usual result is four engines (a ticket DB, a Neo4j CMDB, a Pinecone similar-incident index, a reporting warehouse) with sync jobs between them, and a "single source of truth" that is really four copies drifting apart. This suite is the opposite: the whole ITSM on one DataK3 bucket. CIs, incidents, problems, changes, services, users and groups are the shared spine; the CMDB blast-radius graph, the SLA clock, auto-triage, problem clustering, the major-incident bridge and CAB change control are workflows that JOIN that spine directly — one copy of the rows, queried by content (SQL), by relationship (graph), and by meaning (vector).
The problem — and the money
An ITSM suite is priced per agent seat — 300–2,000 fulfillers at ~$100–$150/month lands a seven-figure
bill before a single custom workflow, and that price buys four systems stitched together. Each workflow
here is worth a tutorial on its own, and has one. The point of the suite is that they compose with
zero glue: the triage engine reads the same incidents core owns; the SLA clock ticks over those same
incidents; the CAB gate scores a change's risk off the same CMDB graph the blast-radius skill assembled.
No connector, no nightly export, no reconciliation.
The payoff is the question a stitched stack cannot answer in one query — "if pg-orders-primary fails,
what breaks, what has already gone wrong on it, and is the fix-it change safe to ship?" On DataK3 that is
three JOINs over one copy of the rows: the database's blast radius of 10 CIs across all 4 business
services (max hop 3), the four incidents on that CI that clustered into a single known error
(PRB2001), and the CAB verdict that the same blast radius makes the permanent fix a cab_review, not an
auto-approve. One bucket, three pillars.
Seven base skills, each independently live-validated, compose into the suite:
| # | Skill | What it owns | Reads (shared master) |
|---|---|---|---|
| 1 | itsm/core | cis, incidents, problems, changes, services, users, groups, ci_edges, incidents.embedding | — (the root) |
| 2 | itsm/cmdb-blast-radius | impacts, service_map, impact_analysis, the graphs cmdb/cmdb_impact | cis, ci_edges, services |
| 3 | itsm/sla-management | sla_definitions, incident_sla, sla_breaches | incidents, services, groups |
| 4 | itsm/incident-management | incident_triage, incident_work_notes | incidents, cis, services, groups, incidents.embedding, cmdb_impact |
| 5 | itsm/problem-management | problem_incidents, known_errors | incidents, problems, changes, incidents.embedding |
| 6 | itsm/change-management | change_approvals, change_calendar, change_policy | changes, cis, services, cmdb_impact |
| 7 | itsm/major-incident | bridges, bridge_events | incidents, cis, services, cmdb_impact |
This page does not re-teach them — follow each link for the mechanics. Here we show how they assemble
into one bucket. The machine-readable manifest is the
suites/itsm contract.
The components teach; the suite ships
Each of the seven posts above is a standalone package you can read and run — that is what they are for, and
each component's own download is still exactly that. But the deployable artifact is the suite.
code/itsm-suite-app is the whole ITSM as one app: a single FastAPI with a
router per component (mounted under /core, /incident-management, /problem-management,
/change-management, /major-incident, /sla-management, /cmdb-blast-radius), one canonical
models.py covering all 23 tables, and plain imports — no importlib loader — over one bucket and one
dodil-appid pool, so seven components mean one sign-in and one bill. That is what you fetch to stamp a
customer system; the component posts are what you read to know why each part is shaped the way it is,
and how to derive a different customer's version. It ships by the ordinary git cycle (repo → CI → registry →
CD): Ship a DODIL app. The tar also carries PLATFORM.md — the
platform invariants, so whoever downloads it gets the rules along with the code.
How it composes
Three rules make seven skills one ITSM.
1. One bucket, 23 disjoint tables. Every skill's tables are globally unique names, so they coexist in a
single bucket with no collision — 8 core + 3 cmdb-blast-radius + 3 sla + 2 incident + 2 problem + 3 change +
2 major-incident. incidents.embedding is a column, not a table; cmdb/cmdb_impact are graphs.
Installing the suite is installing the seven skills, in order, into the same --bucket. The bucket here is
literally named itsm — four characters, so unlike the GL suite's two-character gl (which had to become
gl-suite) it needs no suffix; DataK3 requires a bucket name of at least three.
2. Masters are owned once, joined everywhere. itsm/core owns the seven masters. No other skill
re-declares them — they JOIN them. The triage engine writes incident_triage keyed to the same
incident_id core owns; the SLA clock keys to that same incident; the CAB verdict keys to the same
change_id; the bridge resolves that same incident. One row, many readers — and one live-found
reconciliation: service_id is a string key on cis, incidents and services, so the
impacted-service rollup JOINs cleanly across all three:
In the itsm bucket, show the cross-skill joins that prove shared masters: incident-management's incident_triage joined to core's incidents, and change-management's change_approvals joined to core's changes with the blast radius the cmdb graph produced.
data_sqlincident_triage ⋈ incidents: INC1009 = triaged / database / P1 / g-dba, classified by kimi-k2.6 at confidence 0.90 — the triage engine wrote against the same incident_id core owns, and advanced its state. change_approvals ⋈ changes: CHG5001 = cab_review / high, 4 impacted business services, blast_radius_json spanning 9 CIs — the CAB verdict keyed straight to the change core owns, its risk derived from the cmdb_impact graph. No copy, no join table between products.
export BUCKET=itsm
# incident-management (incident_triage) ⋈ itsm/core (incidents) — same incident_id, two skills
dodil data sql -b "$BUCKET" "
SELECT t.incident_id, i.state, t.category, t.priority, t.assignment_group, t.model_id
FROM incident_triage t JOIN incidents i ON i.id = t.incident_id
WHERE t.is_duplicate = false"
# 1009 | triaged | database | P1 | g-dba | kimi-k2.6
# change-management (change_approvals) ⋈ itsm/core (changes) — same change_id, risk from the graph
dodil data sql -b "$BUCKET" "
SELECT a.change_id, ch.approval_state, a.decision, a.risk, a.impacted_service_count
FROM change_approvals a JOIN changes ch ON ch.id = a.change_id
WHERE a.change_id = 5001"
# 5001 | cab_review | cab_review | high | 43. One graph, assembled once. This is the subtle one. cmdb and its reverse cmdb_impact are created
from a node table (cis) and an edge table by a CREATE GRAPH that snapshots its edges at creation —
edges added afterwards are invisible. In v1 the only contributor of CI edges is itsm/core, so the
single CREATE GRAPH is deferred to itsm/cmdb-blast-radius at position 2 (right after core loads
every edge) — core inserts nodes+edges but runs no CREATE GRAPH. Then the blast radius traverses it — and
it must be typed, or part_of composition leaks in and over-counts:
In itsm, run the single deferred graph assembly (cmdb + the reverse cmdb_impact) after core has loaded all ci_edges, then show the blast radius of pg-orders-primary — and prove the traversal is typed by narrowing impact_rels to exclude part_of.
data_pgcmdb_impact assembled once over core's fully-loaded 14 ci_edges. Blast radius of pg-orders-primary (CI 1) = 10 CIs across all 4 business services, max hop 3: hop 1 checkout-api / orders-api / accounts-api / reporting-etl / pg-orders-replica, hop 2 payments-gateway / web-storefront / search-index, hop 3 mobile-app-bff / cdn-edge. The typed subtlety, found live: rebuild impacts with impact_rels excluding part_of and the same blast is 9 CIs — Online Checkout falls from 5 to 4, because mobile-app-bff hangs off web-storefront by a single part_of edge and is reachable no other way. Type your traversal.
# cmdb-blast-radius runs the ONE assembly, after core's edges are all written (snapshot rule)
dodil data pg -b "$BUCKET" "CREATE GRAPH cmdb NODES (cis KEY id) EDGES (ci_edges SRC src DST dst)"
dodil data pg -b "$BUCKET" "CREATE GRAPH cmdb_impact NODES (cis KEY id) EDGES (impacts SRC src DST dst)"
# blast radius of pg-orders-primary (CI 1) — hop-ranked, names hydrated in one statement
dodil data pg -b "$BUCKET" "
SELECT k.hop_distance, c.name, c.service_id
FROM graph_khop('cmdb_impact', 1, 5, 'out') k JOIN cis c ON c.id = k.node
ORDER BY k.hop_distance, c.id"
# 1 checkout-api · orders-api · accounts-api · reporting-etl · pg-orders-replica
# 2 payments-gateway · web-storefront · search-index
# 3 mobile-app-bff · cdn-edge -> 10 CIs, 4 business services, max hop 3
# the precomputed rollup every other component reads, rather than re-traversing
dodil data sql -b "$BUCKET" "SELECT impacted_ci_count, max_hop, impacted_services_json
FROM impact_analysis WHERE ci_id = 1"
# 10 | 3 | ["Customer Accounts","Internal Reporting","Online Checkout","Order Management"]IMPORTANT
The composition subtlety, found live. A traversal that is correct for one skill can be wrong once the
suite shares its graph. impacts is built only from rels in impact_rels (default: all three). A
blanket reverse over every ci_edges row over-counts when part_of (composition) shouldn't propagate
failure — the ITSM analog of the CRM family-rollup bug where an untyped hop leaked partner_of edges
($66k, not $36k). Live-proven on this estate: pg-orders-primary's blast is 10 CIs with all three
rels but 9 once part_of is dropped, and Online Checkout falls 5 → 4 — mobile-app-bff reaches
the graph by exactly one part_of edge to web-storefront and by nothing else. One CI and one business
service is the difference between paging the right team and paging everyone. Assembling into one graph is
the whole point — but every graph read must say which edges it means.
What composing actually caught
The argument for a suite is usually stated as convenience — one bucket, one deploy. That undersells it.
When the seven components were finally stood up together on one bucket (2026-09-08), six integration
bugs surfaced that every component had hidden while it ran alone, because alone each one was internally
consistent and passed its own ## Test:
pgvector.sqlalchemy.Vectorcannot be read back through the ORM on DataK3. Over a binary-format result — which is what the ORM uses — DataK3 returns aVECTORcolumn as a native float array, so psycopg hands SQLAlchemy alistwhile upstream's result processor expects the text form and calls.split()on it. Everysession.get(Incident, …)in the suite died withAttributeError: 'list' object has no attribute 'split'. Writes are fine; a rawtext("SELECT embedding")is fine; a package whoseincidentsstub omits the column never selects it at all — which is exactly why nobody saw it until the models were unified. Fixed with a tolerantVectorsubclass, byte-identical in all fourmodels.pythat declare a vector column. This one is platform-wide, not ITSM-specific — it hits any DODIL build whose ORM selects a vector column — and the subclass is a workaround for a filed platform issue, not the end state. → itsm/core- Two schema drifts.
incidents.service_idwasBigIntegerin one package andStringin every other (the masters are strings —svc-orders); andchanges.typewas mapped as the attributechange_type, which breaksdb.upsertwith an "unconsumed column names" error and breaks the read-merge-upsert idiom{c.name: getattr(row, c.name) …}with anAttributeError. Canonical rule now stated inmodels.py: the ORM attribute name IS the column name. One canonicalmodels.pyin the suite app is what makes the drift impossible in the first place. → itsm/incident-management, itsm/problem-management - A read-your-writes assumption.
cluster'slink_changepath read a row it had written in the same open transaction — DataK3 stages the write log until commit, sos.get(Problem, pid)returnedNoneand it died on'NoneType' object has no attribute 'id'. Fixed with a commit between the write phase and the back-link phase: two transactions, not one. → itsm/problem-management - A state machine with no reachable entry. The problem engine spawned its permanent-fix change in
state='new', and change management only assesses from{'assess', None}— so the change a problem exists to deliver could never clear the CAB:409 change is frozen in state 'new', forever, silently, on every retry. Both components were correct alone; together they were a deadlock, and the failure was a stable 409 rather than a crash, so nothing alerted. → itsm/change-management - And the one worth remembering: one ticket took the SLA engine down for the whole estate. Core's create
route did not stamp
opened_at, so a ticket landed with a NULL open time. The clock computes both due dates asopened_at + minutesacross every open incident, so that single row raised aTypeErrorand every subsequent tick 500'd — freezing everyone else's breach flags. The dashboard did not go red. It went stale, and stale looks exactly like "nothing is breaching". Fixed on both sides: the producer stamps t0, and the clock now skips and counts uncomputable rows (skipped_no_clock), because a per-tick engine must never be one bad row away from stopping. → itsm/sla-management
Two further findings came from the bridge and the clock finally sharing a bucket. The clock never retired
a ticket somebody else resolved — INC1006 still read in_progress, resolve_breached, escalation_level 2
long after the major-incident bridge had closed it, because the evaluation loop only walks open incidents;
tick now has a retire pass. And a resolved breach paged a manager forever: the on-call rollup grouped
unnotified breaches with no reference to whether the ticket was still open, so INC1006's level-2 breach sat
on [email protected]'s list after the bridge stood down. /sla/oncall now joins incidents — while
the sla_breaches log itself is deliberately untouched, because it is an audit record: "what happened"
and "what do I owe right now" are two different questions over the same table.
None of these are exotic. They are the ordinary consequence of components that share tables being verified in isolation, and the lesson is worth more than any individual fix: components validated separately are internally consistent and still wrong together. That is the reason this suite is presented as one system rather than seven tutorials that happen to sit in the same folder.
Auth — config at the edge, a role gate in the app
On Ignite, end-user login is configuration, not code. The suite deploys with the itsm-suite
dodil-appid pool attached (user_pool: itsm-suite in .dodil/deploy.yaml) and the per-cluster Ignite
gateway runs the entire browser login at the edge — PKCE S256 against the pool issuer, an AEAD-sealed
host-only session cookie, single-flight refresh, EdDSA verification against trust anchors this app does not
hold — then injects the verified identity into every request it forwards: X-Dodil-User (sub, email,
connection, app_roles), X-Dodil-User-Jwt (the raw verified token, carrying the catalog-expanded
permissions claim) and X-Dodil-Auth-Source (pool for an app end-user, platform for an operator or
service-account invoke). Any inbound copy of those headers is stripped first, on every mode and every
principal, so a caller can never forge them.
What survives in the package is a small auth.py that ships no verifier — no JWKS client, no
issuer/audience env, no crypto dependency, and no pyjwt in requirements.txt. It reads the injected
header and keeps the one job the app still owns: role-based gating.
Permissions are namespaced <module>:<object>:<verb>, so one customer pool carries every ERP module's
roles without collision (itsm:change:approve is not crm:change:approve). Across all seven components the
audit left exactly four gates standing:
| Permission | Gates | Why this one and not the rest |
|---|---|---|
itsm:change:approve | change-management: assess, transition, and the change-policy upsert | accepting the risk of a production change. change_approvals.decided_by records the gateway-vouched user — and editing the policy is editing the gate, so it needs the same permission as passing it |
itsm:major:declare | major-incident: declare a bridge | declaring a major incident pages the org. bridges.declared_by records who |
itsm:incident:resolve | major-incident: the bridge-state transition | the only route in the module that writes incidents.state='resolved' — which is what stops the SLA clock. Deliberately separate from itsm:major:declare: whoever declares an incident is not automatically whoever may call it over |
itsm:cmdb:rebuild | cmdb-blast-radius: the graph assembly | it TRUNCATEs impacts + service_map and DROP+CREATEs both graphs. Every other component's blast radius is whatever this route leaves behind, and a half-assembled graph fails quietly — a smaller blast radius reads as good news |
Four of the seven components ended with zero gates, on purpose: itsm-core,
itsm-incident-management, itsm-problem-management and itsm-sla-management. CMDB CRUD, triage and
clustering are the service desk's ordinary work — a permission that every agent on the desk must hold is
ceremony, and ceremony is exactly what an auditor discounts. Three gates the pre-conversion design had
predicted were deleted in the audit: incidents:triage, problems:write, and cmdb:write (that last
one guarded an idempotent per-CI precompute, which re-running changes nothing). One gate the roadmap had
not predicted was added: itsm:cmdb:rebuild. The audit's question was never "is this a write?" but
"does this accept risk, page people, stop a clock, or destroy something?"
The pool is created once for the whole suite, with the role catalog those gates check:
Create a dodil-appid pool itsm-suite with email+password, and set its role catalog: a service-desk agent needs no special permission; a change manager may approve changes; an incident commander may declare a major incident and resolve it; a CMDB admin may rebuild the graph.
Pool itsm-suite created — issuer https://appid.dodil.io/ihdiash/itsm-suite, audience pool:itsm-suite, email+password (local) enabled. Catalog set: agent = (no permissions); change-manager = itsm:change:approve; incident-commander = itsm:major:declare, itsm:incident:resolve; cmdb-admin = itsm:cmdb:rebuild. A user's next token carries app_roles plus the catalog-expanded permissions claim the gates read.
dodil appid pool create itsm-suite --with-local
# issuer: https://appid.dodil.io/ihdiash/itsm-suite audience: pool:itsm-suite
dodil appid roles set itsm-suite \
agent= \
change-manager=itsm:change:approve \
incident-commander=itsm:major:declare,itsm:incident:resolve \
cmdb-admin=itsm:cmdb:rebuildNote that agent — the service desk, the largest role by headcount — holds no permissions at all. It
does the ungated work, which is most of the module.
The two-plane rule is unchanged: the pool identifies the user; the app still reaches DataK3 through
its own service account (sa_token.py mints and refreshes a client_credentials token for the pg-wire
password) — an app-user is never a bucket principal. Locally, with no gateway in front of uvicorn, opt in
to a stub identity with DEV_ALLOW_ANON=1; the stub carries no permissions unless you grant them
(DEV_USER_PERMISSIONS=itsm:change:approve,…), so the gated routes stay gated on a laptop too. Today the
pool is email+password (local); oauth/oidc/saml corporate SSO switch on per pool later, with no app
change. Full flows: App authentication and
App roles.
The route with no identity at all
POST /sla/tick carries no identity dependency whatsoever — not a permission, not even
Depends(current_user). It is a machine heartbeat, called by a service account over a platform
invoke, which carries no X-Dodil-User at all. A current_user dependency there would 401 the clock:
the engine would stop and every incident's breach flags would silently go stale — the failure mode of an SLA
system that reports green. Exposure is the ingress's job (public_invoke=false), not a user permission.
This applies to every recompute route in every module — a rollup, a re-materialization, an embedding
backfill: if a machine calls it, gate the door, not the caller.
The scheduler — something has to drive the clock
Ignite is request-invoked, and there is no server-side scheduler. That is fine for every other workflow here, which is triggered by a person or an event. It is not fine for the SLA clock: an SLA clock that only advances when somebody loads a page is not an SLA clock. A deployment must pick one of two patterns, and say which:
- (a) an always-on pinned app — deploy with
--reserved 1 --max-replicas 1and run a loop inside the pod that callstick()on an interval. Self-contained, and the clock never depends on anything outside DODIL; in exchange the app never scales to zero, so it costs a warm replica. - (b) an external scheduler — cron, a CI timer, or any orchestrator POSTing
/sla/tick. The app stays scale-to-zero, but the clock now depends on infrastructure outside DODIL.
Either way the caller is a service account over a platform invoke, not a browser user — which is
precisely why /sla/tick must not sit behind current_user. This is a genuine platform gap rather than
a design preference, and ITSM is where it is most visible to a buyer: everything else on the platform can be
button-triggered; a contractual SLA clock cannot. Full treatment in
itsm/sla-management.
Scaffold it — the one-shot
With the DODIL MCP connected, one prompt scaffolds the full suite in order into one bucket:
Scaffold the full ITSM suite in one bucket (suites/itsm).
Bucket: itsm. Seed the shared storefront demo. Install the seven base skills IN ORDER into that one bucket:
1. itsm/core — 8 masters + ci_edges + incidents.embedding VECTOR(2048). service_id is a STRING
key on cis/incidents/services. Stamp opened_at on every incident created.
Populate cis+ci_edges but DEFER CREATE GRAPH.
2. itsm/cmdb-blast-radius — build service_map + the reverse impacts (typed to impact_rels), then run the
SINGLE CREATE GRAPH cmdb + cmdb_impact over core's fully-loaded edges.
3. itsm/sla-management — SLA policy + the clock over core.incidents (pure SQL, no Models). /sla/tick is
ungated: it is a machine heartbeat, not a user action.
4. itsm/incident-management — dedup + triage over incidents.embedding + cmdb_impact; start the SLA clock.
5. itsm/problem-management — cluster incidents by embedding into problems + known-errors; spawn the
permanent-fix change in the state change-management assesses from.
6. itsm/change-management — CAB gate; risk = the cmdb_impact blast radius; deterministic freeze floor.
7. itsm/major-incident — declare a bridge off an incident, scope it from cmdb_impact, run
open -> mitigating -> resolved, and resolve the underlying incident.
Then prove the assembly: 23 tables coexist; incident_triage⋈incidents and change_approvals⋈changes join;
the blast radius of pg-orders-primary is 10 CIs across 4 business services; a P1 SLA breach exists and is
retired once the bridge resolves it. Ask me the union questions once (bucket, seed, overlay, impact_rels,
categories) — don't re-ask what composition already answers.Want an industry cut? Add overlay: finserv | manufacturing — an additive diff on top of the same 23
tables (a compliance hard gate + change_audit, or OT CIs + maintenance_windows), never a fork.
Building just part of it
You don't have to install all seven. To build a subset, install itsm/core first —
it owns the seven masters + ci_edges + incidents.embedding every workflow reads — then add only the
workflow skills you want. Each workflow tutorial names its master dependencies (its skill contract's
consumes block is the machine-readable list): incident-management
reads incidents/cis/services/groups; change-management reads
changes/cis/services; major-incident reads incidents/cis/services
plus the blast graph. Each workflow can also run fully standalone on an empty bucket — it ships a
stub-masters step that creates just the masters it reads — but once two workflows share the same masters,
install itsm/core once instead of letting each stub its own. (Stubbed masters are precisely how two of the
six integration bugs above stayed hidden.) And skip the graph (no CREATE GRAPH) unless you install
itsm/cmdb-blast-radius, which owns the single deferred assembly;
incident-, change- and major-incident management degrade gracefully without it (no-blast triage; the
non-graph risk floor).
The questions, asked once
Composition removes the redundant asks. The suite interview is short:
- Asked once (shared): the
bucket,seed_data, the industryoverlay,impact_rels(shared by cmdb-blast-radius, change-management's risk and major-incident's scope), andcategories(shared by the incident triage gate and the problem RCA gate — one answer, both gates). - Removed by composition: incident/problem/change/major never ask "hand-feed incidents or seed?" — their incidents and changes come from core; change-management never asks "build a CMDB?" — the graph comes from cmdb-blast-radius; incident-management never re-declares the SLA clock table — it's owned once by sla-management.
- Still per-workflow (genuine knobs): the
priority_scheme, the changeauto_approve_risk, thesla_targets, thefreeze_windows, thecluster_min, the bridgeseverity_scheme. Real best-practice decisions — the overlay defaults them, you tune them.
Verify
The full single-bucket assembly was live-validated on 2026-09-08 (org IHDIASH, bucket itsm, left up) —
the composition itself, not a re-test of each skill. The estate is 13 CIs / 14 typed edges / 4 business
services / 5 groups / 13 incidents / 5 changes:
# 1) 23 tables from all seven skills coexist in ONE bucket, NO name collision
dodil data table list -b itsm # 23 (8 core + 3 cmdb + 3 sla + 2 incident + 2 problem + 3 change + 2 major)
# 2) shared master, two skills: incident-management's incident_triage ⋈ core's incidents
dodil data sql -b itsm "SELECT i.state, t.category, t.priority, t.assignment_group, t.model_id
FROM incident_triage t JOIN incidents i ON i.id = t.incident_id WHERE t.incident_id = 1009"
# triaged | database | P1 | g-dba | kimi-k2.6 (precedent: INC1003 / INC1005 / INC1004, all g-dba)
# 3) the vector pillar answered before the model was called: INC1011 deduped against INC1010
dodil data sql -b itsm "SELECT is_duplicate, master_incident_id, rationale
FROM incident_triage WHERE incident_id = 1011"
# true | 1010 | "Near-duplicate of open incident 1010 (cosine 0.079 < dedup_threshold); linked, not
# triaged fresh." -> no classification call, no token spend
# 4) ONE graph, assembled once by cmdb-blast-radius; the typed blast radius of pg-orders-primary
dodil data sql -b itsm "SELECT impacted_ci_count, max_hop, impacted_services_json
FROM impact_analysis WHERE ci_id = 1"
# 10 | 3 | ["Customer Accounts","Internal Reporting","Online Checkout","Order Management"]
# (9 CIs once part_of is dropped — Online Checkout 5 -> 4, mobile-app-bff falls out)
# 5) problem clustering over incidents.embedding: 4 incidents under one problem, one of them
# triaged 20 seconds earlier by a DIFFERENT component
dodil data sql -b itsm "SELECT incident_id, ROUND(similarity,4) FROM problem_incidents
WHERE problem_id = 2001 ORDER BY incident_id"
# 1003 0.0000 · 1004 0.0813 · 1005 0.1416 · 1009 0.1094 (all inside the 0.25 radius)
dodil data sql -b itsm "SELECT number, state, known_error, related_change_id FROM problems WHERE id = 2001"
# PRB2001 | known_error | true | 3001 (KE2001 published, CHG3001 spawned as the permanent fix)
# 6) the CAB scored those changes off the SAME graph — and the freeze floor overrode the model
dodil data sql -b itsm "SELECT change_id, decision, risk, impacted_service_count FROM change_approvals
ORDER BY change_id"
# 3001 cab_review medium 4 · 5001 cab_review high 4 · 5002 auto_approve low 0
# 5003 cab_review high 1 · 5004 cab_review high 2 <- freeze-window conflict, deterministic floor
# 7) the SLA clock over shared incidents — three consecutive ticks, byte-identical
# {"evaluated":6,"breaches":2,"escalations":1} incident_sla 6 rows, sla_breaches 2 rows
# INC1006 (P1, 240-min resolve target) resolve_breached, escalation_level 2 -> [email protected]
# INC1007 (P3, new for 3h against a 60-min response target) response_breached
# 8) and the two components moving together: the bridge resolves the incident, the clock retires it
dodil data sql -b itsm "SELECT bridge_id, severity, state, impacted_ci_count, impacted_service_count
FROM bridges WHERE bridge_id = 'mi-1006'"
# mi-1006 | SEV1 | resolved | 5 | 1 (Online Checkout — scoped from cmdb_impact, not guessed)
# the next tick then read evaluated 5, escalations 0, retired 1The 23-table coexistence, the cross-skill JOINs, the single CREATE GRAPH, the typed blast radius and its
part_of contrast, the dedup that never called a model, the four-incident cluster, the CAB verdicts with
the freeze floor overriding the model, the SLA breach and its retirement by the bridge are all proven live.
The Ignite engines each skill deploys are validated in their own tutorials; the suite test is the
data-plane composition — that seven skills share one bucket, one set of masters, and one CMDB graph
without collision.
NOTE
The bucket is left up and still being written to, so the counts above are a record of the 2026-09-08 validation run rather than a snapshot you can expect to reproduce byte-for-byte today — later tickets and later ticks have moved the SLA rows on. The structural facts (23 tables, the 10-CI blast radius and its 9-CI typed contrast, the cluster membership) are functions of the seed data and do reproduce.
Connect your tools
Everything the seven skills wrote lives in the one DataK3 bucket, reachable by your own stack — psql and
pgvector drivers over the Postgres wire, a Neo4j driver over Bolt against cmdb/cmdb_impact, gRPC for the
table engine. data connect itsm prints the endpoints; point BI, a dashboard, or your app straight at the
live rows. Full, live-validated walkthrough: Connect your tools.
Composes
This page is a composition, not a fork:
- Seven base skills —
itsm/core,itsm/cmdb-blast-radius,itsm/sla-management,itsm/incident-management,itsm/problem-management,itsm/change-management,itsm/major-incident— each its own tutorial and its own## Test. - The suite manifest (
suites/itsm) — the ordered install DAG, the shared-master wiring, the single deferred graph rule, and the union Q&A above. - The suite package (
code/itsm-suite-app) — the seven components as one deployable FastAPI, one canonicalmodels.py, one bucket, one pool. - Overlays (
itsm/overlays/*) — additive industry diffs (finserv, manufacturing) that apply on top of the assembled suite.
Read the seven to learn each workflow; read this to assemble them into one ITSM on one bucket.