Skip to content

Organize work by org and project

View as Markdown

This guide gives each customer an org with its projects, runs and reads their work by project or org, and deletes an org.

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 workspace 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. For what kindgi deploy registers, that's the project the deploy named (--project), else the default project. A turn in a conversation stays in the conversation's project.

An agent, flow, tool or set of test cases 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, and nothing is written. Its code names the kind, such as agent-project-mismatch, flow-…, tool-… or test-set-project-mismatch. 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 cases 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.

  • A runtime, such as kindgi dev, with Kindgi 0.2's client and CLI.
  • Node 22.12 or later, Python 3.11 or later, or Java 17 or later, for the client.
  • KINDGI_API_URL and KINDGI_API_TOKEN set, or a running kindgi dev, which the client finds.
  • An agent, to run in the customer's project. This guide uses acme-ops.echo-agent.

Create the customer's org and a project in it once, then run the customer's work there. Ask your coding agent: "I want each of my customers to have an org with a project, and their work to run there." For reference, the script looks like this. It prints the run's status and the org's runs:

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 projects create --name=Support --org=<org-id> makes a project and prints it with its id. Its slug is the name as a slug (support) unless --slug gives one.
  • kindgi projects rename <project-id> --name=<name> renames one, and --slug changes its slug.
  • kindgi projects list lists the projects with their ids.
  • kindgi runs start … --project=<project-id> runs in a customer's project.

In the console, a tenant admin makes one with New project on the workspace dashboard's Projects card: a name, a slug made from the name until you edit it, an org if the workspace has any, and what it's for. The new project then opens. A project's admins (an admin of its org, or a tenant admin, is one) rename it with the pencil button in its Dashboard's header, Rename the project. Its slug stays unless you tick Change its slug too. The new slug then follows the name until you edit it, and the dialog says what to change before you next use the CLI on the project. Its id stays; what a new slug changes is under Changing a project's slug, below.

Slugs. An org's slug is unique in the workspace. 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 workspace 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)

Changing a project's slug. A rename is one change: a taken slug is refused, as above, and nothing changes. Once it's renamed, its old slug is free for another project to take. The project's id stays, and the runtime goes by the id, so the project's pages in the console, its runs, keys and triggers, and what's deployed into it keep working, with no redeploy. Only the CLI looks a slug up, each time it runs, wherever you name a project by its slug: kindgi deploy --project, say, or an environment's project in your pack config (environments.<name>.project). Every command that names the project that way looks it up: kindgi deploy, kindgi promote, kindgi environments set, and kindgi secrets and kindgi env under a pack environment (--env). Before you next run one, change the old slug to the new one there. Until you do, such a command is refused, since the slug names no project, unless another project in the workspace has it: one in another org may have it already, or take it later. Then the command acts on that project: a deploy goes into it without a word, and a secret or value is set on it. So if you change a project's slug, name the project by its id in scripts and pack configs, or change them to the new slug before anyone runs one of those commands again.

A workspace has one default project. Asking for a second one answers 409 project-default-already-exists, "The workspace already has a default project".

Give each client its own project and its own pack. kindgi init makes a pack whose ids all start with its pack id, so two clients' packs never share an id:

Terminal window
npx --yes @kindgi/cli@0.2 projects create --name="Acme shop" --org=<org-id>
npx --yes @kindgi/cli@0.2 init acme-shop --template=woocommerce
cd acme-shop
pnpm install
pnpm exec kindgi deploy --project acme-shop

--project takes the project's id or its slug. A slug is unique within an org, so if two orgs each have a project with that slug, the CLI refuses it and names both ids: pass the id instead. Without --project, a deploy is workspace-wide and registers into the default project, as before.

What a deploy into a project does, with the runtime 0.1.8 or later:

  • Its agents, flows, tools and guardrails are registered in that project, and the deployment records it (projectId on GET /v1/deployments/{deploymentId}).
  • kindgi deploy --sync-secrets writes the secrets into that project, so one client's credentials stay in its own project. A workspace-wide deploy writes them workspace-wide. A secret a tool names that's set nowhere yet when it's deployed is read where the sync writes it (runtime 0.1.9 or later). So after a deploy into a project, set it later in that project (kindgi secrets set <name> --env=<env> --scope=project:<project-id>), not workspace-wide.
  • It needs admin on that project (a workspace-wide deploy needs admin on the workspace). An API key limited to another project is refused with 403 key-project-mismatch.
  • A project of another workspace, or none, is 404 project-not-found.
  • A suspended project takes no deploy, where the runtime suspends projects: 409 project-suspended, before anything is written. No run starts in it either: POST /v1/runs answers the same, as do a test run started in it and an Improve run for one of its agents. A test run or an Improve run already going stops at its next case and ends cancelled, with project-suspended in its cancelReason. A delivery to one of its webhook triggers is answered 202 and skipped, with nothing recorded on the trigger's fires.

Deploying the same pack into a second project is refused, because its ids already belong to the first project: 409 tool-project-mismatch (or agent-…, flow-…, guardrail-…), and nothing is deployed. The message names the fix your runtime has. Where each deployment runs as a service of its own, make a copy of the pack for the new client with kindgi init, which gives every id the new pack's id. Where one pack service runs every deployment, the default on any runtime, a copy's code wouldn't run. There, when both projects are in the same org, or neither is in an org, put them in one environment order (kindgi environments set), the second right after the first, and kindgi promote the build to the second instead of deploying it. Projects in two orgs can't share an order, so a client in an org of its own, as this guide sets one up, needs a runtime that runs each deployment as a service of its own.

A run uses its own project's tools, agents, flows, guardrails, data blocks and test cases, and the tools of an MCP server registered for its project, its org or the workspace. Anything else is refused with reference-out-of-scope: a publish, a deploy or a run start that names it answers 400 before anything is written, and a tool call from the run isn't run. What a project's runs can use has the rule and its messages.

To give several clients' projects one tool, run it as an MCP server and register it for their org or the workspace (Give some projects its tools).

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

A record with no project shows only in a list without a scope.

A tool sees the run's project and org, as ctx.projectId and ctx.orgId. So it can check that a record belongs to the customer the run is for, as Check the run's org shows.

A project is in production when the customer's real work runs in it: serving their customers, or processing their live business data. A run in a project in production is a production run, and a run anywhere else is staging. A runtime that counts usage (KINDGI_USAGE_METERING=on) labels each run with its class for usage counting, so test work is counted apart from real work. A run's own read doesn't show it.

On a runtime you run yourself, with neither usage counting nor the production gate or limit below, being in production is only a label: agents, flows, triggers and approvals work the same in a project in production and out of it. The license key doesn't read it either (What a key covers).

To switch it from your app, ask your coding agent: "Put a customer's project in production when they go live." For reference, the commands and the code look like this:

Terminal window
pnpm exec kindgi projects production <project-id> on
pnpm exec kindgi projects production <project-id> status
pnpm exec kindgi projects production <project-id> off

In a Python project that's uv run kindgi …, and in Java or Scala ./kindgiw ….

Each prints one line: whether the project is in production and, if it is, since when. --json prints { projectId, productionSince }. A refusal names its code and exits 1.

In the console, a project's Dashboard says at the top whether it's in production: Live, by a green dot, or Testing mode, by a gray one. Choosing it says since when it's been live, or "Kindgi counts its runs as testing. Once it's live, they count as live use.", and gives a tenant admin the switch: Go live, or Move back to testing. While the workspace may not put projects in production, it says "Going live isn't turned on for this workspace yet.", and gives a tenant admin See how to go live when the runtime names where to go. A refused switch says why in the console's words: at the workspace's limit of live projects, "Move one back to testing first", with a link to get more when the runtime names one, or "Going live isn't turned on for this workspace yet."

  • Tenant admins only, both ways: anyone else gets 403 permission-denied.

  • Setting what's already set changes nothing, and keeps the first productionSince. Reading the project shows it while it's in production.

  • The production gate: a runtime may require the workspace to be allowed to put projects in production, for example once it has paid (KINDGI_PRODUCTION_GATE=on). Until then, putting one in production is refused with 403 production-not-allowed, and the error's details.next says where to go when the runtime names it (KINDGI_PRODUCTION_NEXT_URL). A project already in production stays so, and staging keeps working. GET /v1/tenant says where the workspace stands: productionAllowed, and productionNext while it isn't allowed.

    {
    "error": {
    "code": "production-not-allowed",
    "message": "This workspace can't put projects in production yet. To be allowed: https://platform.example.com/account/billing. Projects not in production keep working as staging.",
    "details": {
    "projectId": "b3bc8d28-8487-4d90-be57-4e522488f10c",
    "next": "https://platform.example.com/account/billing"
    },
    "requestId": "req-04d7cdca-a8ca-4ff6-87ad-9939387223a0"
    }
    }
  • A limit on projects in production: whoever runs the server can set the workspace's projectsInProduction limit (PUT /v1/system/tenants/{tenantId}/limits). With that many of its projects in production, putting another one in is refused with 403 production-limit-reached. Its details give the limit (projectsInProduction), the count (inProduction), and next when the runtime names it. A project already in production answers its repeat unchanged, and taking one out is never refused. Lowering the limit takes no project out of production: the next one waits until the count is under it. GET /v1/tenant shows the limit in limits.

    {
    "error": {
    "code": "production-limit-reached",
    "message": "This workspace's limit is 3 projects in production, and it has 3. To put another in production: https://platform.example.com/account/billing, or take one out of production first.",
    "details": {
    "projectId": "22792d5f-b8cb-4d57-a785-b1f00a57b38e",
    "limit": "projects-in-production",
    "projectsInProduction": 3,
    "inProduction": 3,
    "next": "https://platform.example.com/account/billing"
    },
    "requestId": "req-b455a4ea-669e-431f-9350-3142a35c991d"
    }
    }
  • A runtime whose projects can't be put in production answers 501 production-unsupported.

Delete an org when its customer leaves. Ask your coding agent: "When a customer leaves, delete their org." For reference, the code looks like this, with kindgi and org from the script:

await kindgi.orgs.delete(org.id);
Excerpt
  • 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. That comes after its environments' and MCP endpoints' own records are purged.

Your workspace is the tenant your orgs and projects sit in. A tenant admin can rename it (runtime 0.1.10 or later): its name, its slug, or both. Its id stays, so whatever names it by its id keeps working, your keys and the CLI included.

  • kindgi tenant rename --name="Acme Store" changes its name, and --slug=acme-store its slug. It prints the workspace, as kindgi tenant get does.
  • The TypeScript and Python clients' tenant.update does the same: PATCH /v1/tenant, which answers 204.
  • In the console, use Rename in the header of the workspace's home page.

The console's addresses carry the slug. After a new one, a bookmark or a link with the old slug opens a page that says the address names another workspace, with a link to yours. Another workspace can then take the old slug. Nothing in the CLI or the clients uses it.

A workspace's slug is lowercase letters, digits and single hyphens, at most 63 characters, and unique across the runtime's workspaces. A taken one is refused with 409 tenant-slug-taken, and a closing workspace with 409 tenant-closing.