Skip to content

Organize work by org and project

A project holds runs, conversations, approvals and what they cost. An org groups projects: one org per customer, for example, with a project for each part of their work. Every tenant has a Default project. A run that names no project runs in its agent's or flow's own project: the one it was published in, which is Default for what kindgi deploy registers. A turn in a conversation stays in the conversation's project.

An agent, flow, tool or test set belongs to the project its first version was published into, and never moves. Publishing a version of it into another project is refused with a 409, such as agent-project-mismatch (flow-…, tool-…, eval-suite-project-mismatch), and nothing is written. The refusal doesn't name the project it belongs to, since you may not be able to read that project. A deploy that includes one is refused whole and deploys nothing, even when that agent or flow is unchanged. Publish new versions in its own project.

Reading an agent, flow, tool, test set or guardrail says which project it's in: its projectId, with the runtime 0.1.6 or later. A pack kindgi dev serves from disk has none.

customer-org.ts
import { createClient } from '@kindgi/sdk/client';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
// Once per customer: an org, and a project in it.
const org = await kindgi.orgs.create({ slug: 'acme-customer-one', name: 'Customer one' });
const project = await kindgi.projects.create({ orgId: org.id, slug: 'support', name: 'Support' });
// The customer's work runs in its project.
const run = await kindgi.runs.start({
agent: 'acme-ops.echo-agent',
input: { userMessage: 'Echo hello.' },
projectId: project.id,
});
// Everything in the customer's org, across its projects.
const runs = await kindgi.runs.list({ scope: { kind: 'org', orgId: org.id } });
console.log(run.status, runs.data.map((r) => r.id));
completed [ '68ebe748-4259-4f46-ae21-cf0da3b978c6' ]

Keep the org's and the project's ids on the customer's row in your app. From the CLI, kindgi runs start … --project=<project-id> runs in a customer's project, and kindgi projects list lists the projects with their ids.

Slugs. An org's slug is unique in the tenant. A project's slug is unique in its org, so every customer can have a support project; a project with no org has a slug unique among the projects with none. A taken slug is refused with 409 slug-conflict:

Another org in the tenant has the slug "acme-customer-one"
Another project in its org has the slug "support" (for a project without an org: another project without one)

A tenant has one Default project; asking for a second one answers 409 project-default-already-exists, "The tenant already has a Default project".

The lists take a scope, one project's or every project's in an org: { kind: 'project', projectId } or { kind: 'org', orgId } (Python: scope_kind="project", scope_id=…).

Records from before Kindgi 0.1.3 have no project, so only a list without a scope shows them.

A tool sees the run's project and org (ctx.projectId, ctx.orgId), so it can check that a record belongs to the customer the run is for: Check the run's org.

await kindgi.orgs.delete(org.id);
  • The org is gone: lists leave it out, and fetching it answers 404 org-not-found. Its slug is free at once. Deleting it again changes nothing.
  • Its projects and teams stay, with no org, and so does their work.
  • Its own secrets and their mappings are deleted for good. Its own environments and MCP endpoints are unregistered.
  • Writes to it are refused: setting a secret in it, registering an MCP endpoint in it, and creating or moving a project or a team into it. Creating or moving a project or a team there answers 404 org-not-found, as for an org that never existed: No org with id "0404cbc2-9612-461d-9997-0cbf1d12341a".

Kindgi keeps the deleted org's record until a retention policy on org purges it, after its environments' and MCP endpoints' own records are purged.