Jet

Entities & Layered Context

Every business has a "thing" it works on. An agency has clients, a law firm has matters, a clinic has patients, a real-estate team has properties, an accountant has engagements, a sales team has accounts. Jet calls this an entity and lets installed packs name it.

Everything else — projects, context, memory, workflows — hangs off the entity. That's how Jet stays generic without being useless.

Entity types

The pack you install registers one or more entity types via the workspace_entity_types table. An entity type has:

  • slugclient, matter, patient, ...
  • labelSingular / labelPlural — how the UI labels it (Client / Clients)
  • icon — which icon the sidebar renders
  • schema — which per-entity fields the create form shows

The marketing-agency pack's client schema, for example, defines industry, monthlyAdSpend, targetRoas, primaryPlatforms, contractStart, monthlyRetainerUsd.

Entity instances

An entity instance (the row in workspace_entities) represents one actual client / matter / patient. Its fields column stores the JSON blob that matches the type's schema. Entities also carry:

  • owner_user_id — the primary relationship owner
  • statusactive or archived (never hard-delete)
  • projects — scoped deliverables (campaigns, matters, cases, deals, engagements)

Layered context

Context is what's active in an agent session. Memory is what persists across sessions. Jet's context architecture has four layers; the first three are stored in the tenant's D1 and assembled by the context loader at every agent run, the fourth is runtime.

Layer Scope Table
Institutional Once per tenant — voice, SOPs, standards, case studies context_institutional (singleton)
Entity Per entity — overview, preferences, history context_entity
Project Per project — brief, scope, deadline, sources, format context_project
Session Per conversation — automatic, managed by ChatSession Durable Object (runtime)

How the loader assembles context

When an agent runs, services/contextLoader.ts builds a Markdown block from the first three layers and prepends it to the system prompt. Each layer is labelled so the model knows what it's looking at, and empty layers are skipped.

import { loadContext } from "./contextLoader";

const { markdown, hasInstitutional, hasEntity, hasProject } = await loadContext(db, {
  tenantId,
  entityId: "ent_acme",
  projectId: "prj_q3_campaign",
});

// markdown looks like:
// ## Institutional context
// ### Voice
// Client-facing: confident, direct, data-first ...
//
// ## Client context — Acme Corp
// - **industry:** DTC e-commerce
// - **monthlyAdSpend:** 65000
// ### Overview
// Apex Roofing is a performance client onboarded ...
//
// ## Project context
// **Project:** Q3 Performance Report
// **Due:** 2026-04-25
// ### Brief
// Report period: July 1 – September 30 ...

API reference

Entities

  • GET /entities/types — list the entity types this tenant has (defined by installed packs)
  • POST /entities/types — define a new entity type (or let a pack do it)
  • GET /entities?type=client&status=active — list entities
  • POST /entities — create an entity
  • GET /entities/:id, PUT /entities/:id, DELETE /entities/:id (archive) — CRUD
  • GET /entities/:id/projects, POST /entities/:id/projects — scoped projects

Context

  • GET / PUT /context/institutional — tenant singleton
  • GET / PUT /context/entity/:entityId — per-entity dossier
  • GET / PUT /context/project/:projectId — per-project brief
  • GET /context/preview?entityId=&projectId= — returns the exact Markdown block an agent would receive

Use /context/preview while authoring skills or testing agent behavior — it's the fastest way to debug "why did the agent not know X?" questions.

Next steps

  • Memory layer — what persists between sessions
  • Packs — how entity types get installed
  • Workflows — orchestrating agents with entity + project context