v0.34.0 · Bitemporal history

The Knowledge Graph for TypeScript

Define your ontology with Zod. Query with a fluent builder. Extend it at runtime when your agents need more.

GraphUserPostGroupRoleZodTypes

Define. Connect. Query. Evolve.

Your schema is your database. Your types are your queries.

import { defineNode, defineEdge, defineGraph, createStore } from "@nicia-ai/typegraph";
import { z } from "zod";

// 1. Define your ontology
const Person = defineNode("Person", {
  schema: z.object({ name: z.string(), role: z.string() })
});
const Project = defineNode("Project", {
  schema: z.object({ name: z.string(), status: z.string() })
});
const worksOn = defineEdge("worksOn");

const graph = defineGraph({
  id: "my_app",
  nodes: { Person: { type: Person }, Project: { type: Project } },
  edges: { worksOn: { type: worksOn, from: [Person], to: [Project] } }
});

// 2. Query with full type safety
const results = await store.query()
  .from("Person", "p")
  .whereNode("p", p => p.role.eq("Engineer"))
  .traverse("worksOn", "e")
  .to("Project", "proj")
  .select(ctx => ({ person: ctx.p.name, project: ctx.proj.name }))
  .execute();
import { defineNode, searchable, embedding } from "@nicia-ai/typegraph";
import { z } from "zod";

// 1. Declare semantic + fulltext fields right in the Zod schema
const Document = defineNode("Document", {
  schema: z.object({
    title: searchable({ language: "english" }),
    body:  searchable({ language: "english" }),
    embedding: embedding(1536),
  })
});

// 2. Hybrid retrieval: BM25 fulltext + vector similarity, fused with RRF
const hits = await store.search.hybrid("Document", {
  limit: 10,
  vector:   { fieldPath: "embedding", queryEmbedding: await embed(q) },
  fulltext: { query: q },
});

// 3. Or compose fulltext into any graph traversal — authorised search,
//    multi-tenant filters, anything you can already express in a query.
const scoped = await store.query()
  .from("User", "u").whereNode("u", u => u.id.eq(currentUserId))
  .traverse("canRead", "e").to("Document", "d")
  .whereNode("d", d => d.$fulltext.matches(query, 10))
  .select(ctx => ctx.d).execute();
import { defineGraphExtension } from "@nicia-ai/typegraph";

// 1. An agent proposes a typed kind from observed data.
const proposal = defineGraphExtension({
  nodes: {
    Paper: {
      description: "An academic paper inferred from the corpus",
      properties: {
        title: { type: "string", minLength: 1 },
        doi:   { type: "string", minLength: 1 },
        year:  { type: "number", int: true, min: 1900, max: 2100 },
      },
      unique: [{ name: "paper_doi_unique", fields: ["doi"] }],
    },
  },
});

// 2. Operator approves; commit atomically as a new schema version.
const evolved = await store.evolve(proposal);

// 3. Read and write the new kind dynamically — no codegen, no redeploy.
const papers = evolved.getNodeCollection("Paper")!;
await papers.create({
  title: "Attention Is All You Need",
  doi:   "10.5555/3295222.3295349",
  year:  2017,
});
import { createStore, asRecordedInstant } from "@nicia-ai/typegraph";

// 1. Turn on recorded/system-time capture for committed writes.
const store = createStore(graph, backend, { history: true });

// 2. Capture a deterministic recorded-time anchor after an agent decides.
await store.nodes.Decision.create({ answer: "approve source A" });
const decisionTime = await store.recordedNow();
if (decisionTime === undefined) throw new Error("no recorded history yet");

// 3. Later, replay the exact graph the agent saw.
const replay = store.asOfRecorded(decisionTime);
const sameAnswer = await replay
  .query()
  .from("Decision", "d")
  .select(ctx => ctx.d.answer)
  .first();

// Valid time and recorded time can be pinned independently.
const whatWeKnewThen = store
  .asOf("2026-01-01T00:00:00.000Z")
  .asOfRecorded(asRecordedInstant("2026-02-01T00:00:00.000Z"));

The graph lifecycle

Stop stitching graph tools together.

Keep schema, search, reasoning, merge, and history in one typed system.

Stay in the loop

Occasional updates on new features, guides, and releases. No spam.