# Organizations

Every signed-in person has a **personal** (signup) organization. You can also
create **team** organizations. Resources (packages, secrets, jobs, and so on)
belong to one org. Handles are permanent: `@slug` does not rename.

The org is the billing and data boundary: its resources, usage, limits, and bill
stay inside it. Inside an org, grants control access. Owners hold everything,
Members hold the org basics (`org:read`, `member:read`, `team:read`,
`search:read`), what [member permissions](#member-permissions) allow, and what
they are granted. An outside collaborator holds catalog search (`search:read`)
plus their grants. See [Share a package](/docs/package-sharing) for presets and
grants.

## Member permissions

Like a code host's "members can create repositories" setting, an Owner chooses
what every Member of a team org can create without a grant: **Organization
settings**, **Member permissions**, or `orgUpdate`.

| Setting                              | Default | Gives every Member                                                                 |
| ------------------------------------ | ------- | ---------------------------------------------------------------------------------- |
| Members can create packages and apps | Off     | `package:create`: save, fork, and first-publish packages, repos, and their apps    |
| Members can create credentials       | On      | `secret:create`, `integration:create`: new secrets, OAuth connections, MCP servers |

```json
{ "members_can_create_packages": true, "members_can_create_credentials": true }
```

Owners can always create. Whoever creates something gets full (`manage`) access
to it. Changing an existing package or credential still needs its own write
permission, so these settings never let a Member edit what someone else made.
The settings are enforced on the server for every path (MCP, the API, the CLI's
first publish and repo push, and the website). A Member who is refused gets an
error naming the setting and the Grants page.

To let only some people or teams create, leave a setting off and use **Grants**,
**Create access** (or `accessGrant` with `resource_type: "org"` and
`permissions: ["package:create"]`, `["secret:create"]`, or
`["integration:create"]`). Those grants still apply when a setting is off.

Owners can also turn off
[usage warnings](/docs/platform-efficiency#usage-warnings) (the 80% and 100%
usage emails and the monthly execute heads-up) with `orgUpdate`
`{ "usage_warnings": false }` or **Organization settings**, **Usage warnings**.

## Org-wide read access

To let a Member read everything of one kind without making them an Owner, an
Owner grants read access on the organization: **Grants** page,
**Organization-wide read access**, or `accessGrant` with `resource_type: "org"`:

```json
{
	"resource_type": "org",
	"resource_id": "<org id>",
	"subject_type": "user",
	"username": "casey",
	"permissions": ["job:read"]
}
```

From the CLI: `kody api accessGrant --org <slug> --params '<that JSON>'`. Add
`"expires_at": "<ISO time>"` for temporary access: the grant stops counting at
that time. A new grant without `expires_at` never expires. Rewriting a grant
without `expires_at` keeps a future expiry but drops one that has already
passed; pass `null` to make it unlimited. `accessList` still shows an expired
grant, with its past `expires_at`, until you rewrite or revoke it.

| Permission         | Reads                                                                                       |
| ------------------ | ------------------------------------------------------------------------------------------- |
| `job:read`         | Every job, workflow, and run (`runList`, `runGet`, the Activity page, Overview run history) |
| `app:read`         | Every package app and its sessions                                                          |
| `package:read`     | Source, skills, and checks for every package and repo                                       |
| `integration:read` | Which integrations and MCP servers exist, never a token                                     |
| `email:read`       | Every inbox and message, including sign-in codes they get                                   |

These are read only. Running, editing, triaging runs, sharing, and using a
credential still need their own grant. Secret values and memories are never
grantable org-wide.

## Org-wide manage for packages and apps

To let a member or team run, change, publish, delete, and share every package
and app in the organization, including ones created later, an Owner grants
manage on the organization: **Grants** page, **Manage every package and app**,
or `accessGrant` with `resource_type: "org"`:

```json
{
	"resource_type": "org",
	"resource_id": "<org id>",
	"subject_type": "team",
	"team_slug": "platform",
	"permissions": [
		"package:read",
		"package:execute",
		"package:write",
		"package:publish",
		"package:delete",
		"package:manage_access",
		"app:read",
		"app:execute",
		"app:manage_access"
	]
}
```

Any subset works, for example only `package:execute` and `app:execute` to run
everything. The grant resolves live, so a package created tomorrow is covered at
once and revoking the grant removes access everywhere.

- **Owner only.** A member with `member:write` can manage read and create access
  but cannot add these, the same as `org:write`.
- **Members and teams only.** Everyone in the organization
  (`subject_type: "org"`) and agent profiles cannot hold them org-wide; share
  one package with everyone instead.
- **Unmappable credentials still lock changes.** On a package whose credentials
  cannot be mapped in this organization, `package:write` and `package:publish`
  stay owner-only even with this grant. Running it still works. An Owner unlocks
  changes per package with a grant on that package that sets
  `allow_changes_despite_unmappable_credentials: true`. See
  [Package sharing](/docs/package-sharing).
- Secrets, connections, jobs, and email are not covered. Grant those per
  resource.

## Access for one person

In a team org, an Owner opens **Members**, **Collaborators**, or a person-linked
row on **Grants**, then **Access** (`/@<slug>/-/grants/user/<userId>`). That
page lists what one person can use in the org and **Give access** grants or
revokes a preset on specific packages, apps, and credentials. Team and everyone
grants show as read-only. Owners already hold everything. Use the package
**Collaborators** tab to share one package; use **Access** for one person across
resource types. Details:
[Share a package](/docs/package-sharing#access-for-one-person).

## Delete a team

`teamDelete` (`DELETE /v1/orgs/teams/{team_id}`, or pass `team_slug`) needs
`team:write`, the same as creating a team. It is a soft delete and lands in the
org audit log as `team.deleted`. Members stay in the organization but lose every
grant they held through the team right away; Kody ends their open
execution-context leases. The grant rows stay for history and never apply to
another team. The slug is free again: a new team with the same slug starts with
no grants.

## Personal vs team

|                    | Personal                                                | Team                                                                                                                                                                                                                                                               |
| ------------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Created            | At signup (slug = username)                             | `orgCreate` or Create organization                                                                                                                                                                                                                                 |
| Members / teams    | No. Create a team org to invite people or group them    | Yes                                                                                                                                                                                                                                                                |
| Package grants     | `accessGrant` / grant invites / package invite links    | Same, plus the Grants page and per-person **Access** (`/@<slug>/-/grants/user/<userId>`)                                                                                                                                                                           |
| Resource web pages | Secrets, jobs, repositories, apps, agents, and the rest | Every resource page reads the org. Agents lists connections approved for that org; **Repositories** lists packages you can see plus every plain repo the org owns (API/CLI still say `package`); **Apps** (`/@<slug>/-/apps`) lists packages with `kody.app.entry` |

Switch orgs from the workspace name at the top of the workspace rail (it keeps
the section you are on), from the avatar menu's Manage group, or pick the org at
MCP consent. Overview is `/@<slug>/-/overview`. One connection binds one org.

## Link to your own pages with `/@me`

`/@me` always means the signed-in person's own (personal) account. Any path
under it redirects to the same path on your handle, so
[`kody.codes/@me/-/profiles`](https://kody.codes/@me/-/profiles) opens
`/@<your-username>/-/profiles`. The rest of the path and the query string are
kept. Signed-out visitors sign in first and then land on the resolved page. `me`
is reserved, so no person or org can claim that handle.

Handy settings links: [`/@me/-/settings`](https://kody.codes/@me/-/settings),
[`/@me/-/secrets`](https://kody.codes/@me/-/secrets),
[`/@me/-/agents`](https://kody.codes/@me/-/agents),
[`/@me/-/apps`](https://kody.codes/@me/-/apps),
[`/@me/-/email`](https://kody.codes/@me/-/email),
[`/@me/-/jobs`](https://kody.codes/@me/-/jobs),
[`/@me/-/activity`](https://kody.codes/@me/-/activity), and
[`/@me/-/waiting`](https://kody.codes/@me/-/waiting).

Agents: when a doc shows `/@<slug>/-/...` for the person's personal account and
you don't know their username, link `https://kody.codes/@me/-/...` instead. Use
the real slug for team orgs: `/@me` never points at a team org.

## Public org page

A team org's page at `kody.codes/@<slug>` is **Public** by default: anyone can
see its name, photo, bio, and public packages. Members are never listed, and
private or hidden packages never show. Pick **Private** when you create the org,
or later in **Settings** (Owners only), or pass `profile_visibility` (`public` |
`private`) to `orgCreate` / `orgUpdate`. A private org's page is a not-found
page for everyone outside it, the same as a handle that doesn't exist. Members
of a public org see the public page with an **Organization home** link
(`/@<slug>?view=home`); on a private org the home is at `/@<slug>`. A personal
org's page is your account profile, set on Account.

## Invites

An invite waits on the person it is for and shows on the page that sent it:

- **Waiting on you.** Every pending invite you can answer is a card on your
  Waiting page (`/@<org>/-/waiting`) and a row in the Invites panel on your
  Organizations page (`/account/organizations`), each with **Accept** and
  **Decline**. That covers invites addressed to your email or username and
  invite links you opened. An invite sent to your email needs a verified email
  before you can answer it on the web. Declining ends the invite (recorded as
  `invite.declined`).
- **Sent by you.** The page you sent it from lists it until it is answered or
  lapses: **Members** shows invites addressed to people as Pending, with
  **Revoke** on membership invites, plus active Members links; **Teams** lists
  each team's links; a package's **Collaborators** tab lists its links. Every
  link has **Revoke**.

Agents use `inviteAccept` with the token and `inviteRevoke` for addressed
invites.

## How do I invite my whole team?

Use a **Members** invite link when you do not know every email yet, or when many
people should join with the same role. Prefer addressed `inviteCreate` only when
you need to invite one known person (and optionally Owner).

1. On the web: open `/@<org>/-/members`, create an invite link (Member by
   default; optional label, default teams, domains, max uses, expiry), copy the
   credential once, and share it.
2. As an agent: call `inviteLinkCreate` (needs `member:write`) in that org, then
   share the returned URL or tell people to call `inviteLinkAccept` with the
   token. List with `inviteLinkList`. End a link with `inviteLinkRevoke`.
3. CLI/Open API: `POST /v1/access/invite-links`, `GET /v1/access/invite-links`,
   `POST /v1/access/invite-links/accept`, and
   `POST /v1/access/invite-links/{invite_link_id}/revoke`.

## Invite links

An invite link lets someone in without knowing their email or username. You make
one where you would otherwise add the person, and what it gives depends on where
you made it:

| Made on                           | Accepting it                                                                               | Who can make it                 |
| --------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------- |
| **Members** (team org)            | Joins the org as Member or Billing; optional default teams                                 | `member:write`                  |
| **Teams**, one team (team org)    | Joins that team, and the org as Member if they are not in it yet                           | `team:write` and `member:write` |
| A package's **Collaborators** tab | Grants the chosen preset on that package; non-members stay outside collaborators (any org) | Manage on the package           |

Opening a link signs the person in or lets them create an account (every signup
method asks them to choose their own username first), then shows an accept page
with what the link gives and who sent it. Nothing happens until they choose
**Accept**; they can also **Decline**. A signed-in person who opened a link sees
it on their Waiting page and in the Invites panel on their Organizations page
until they accept or decline it, or it stops working. OAuth keeps the join URL
through redirects via `redirectTo`.

Links last 7 days by default (up to 365; every link expires) and can be limited
by number of uses. Members links can also carry an optional label, default
teams, and a verified email domain allowlist. The link is a credential: Kody
stores only a hash, so the full URL is shown once. Each page lists its active
links with uses and expiry; the Members page also shows last used. **Revoke**
ends one right away. A link also stops working when its org is no longer active,
when the team or package it points at is deleted, or when the person who made it
loses access. Someone removed from the org can rejoin with the same membership
link while it is live (idempotent for people who are already members). Team and
package links are spent per person, so removing someone is not undone by the
link they already used.

Agents use `inviteLinkCreate`, `inviteLinkList`, `inviteLinkRevoke`, and
`inviteLinkAccept` (same surfaces as Open API and the CLI). Creating, revoking,
and accepting are recorded in the org audit log as `invite_link.created`,
`invite_link.revoked`, and `invite_link.joined`, with the link's kind, team or
package, label, teams, and preset in the details.

## Automation runs as the person who turned it on

A scheduled job, subscription, or webhook runs only after a person turns it on
for themselves, by approving it on the package's approval page (or, for a job or
subscription, with `enablementEnable`). Turning a cron enablement on also turns
that job's `enabled` flag on and schedules the next tick when the enabler holds
`job:write`. Without `job:write` the enablement still turns on but leaves the
job unchanged: a job that was off stays off, and the enablement result's `job`
field says so (`state: "left_off"`, `missing_permission: "job:write"`); a job
that was already on stays on (`state: "already_on"`). Minting a webhook URL does
not turn it on: the mint returns the approval link, and deliveries fail with
that link until someone approves. A webhook URL runs as one enabler. That person
is the **enabler**, and every run acts as them:

- The run has the enabler's live access in the org, checked on every run. It can
  do only what the enabler could do by hand at that moment, never more.
- Credentials resolve only when the enabler holds Use on them.
- If the enabler loses access (leaves the org, loses `package:execute`, or loses
  Use on a credential the run needs), the enablement pauses and its events are
  dropped. It resumes by itself once access is back. A run never falls back to
  another person or to Owner.
- A paused enablement shows on its enabler's own Waiting page for that org, with
  what the run needed. Other members and Owners do not get that card.
- Usage counts against the org that owns the trigger, and the run is audited to
  the enabler.

Some actions need a live person, even when the enabler is an Owner. No job,
webhook, subscription, or inbound email handler can do them, and neither can
package code one of those started or a package app's server code:

- Role and membership changes: adding or removing members, changing a role,
  invites, and grants that carry member or billing permissions.
- Billing changes: plan, seats, payment, wallet, budgets, and promo codes.

Do these from the web app, an MCP or CLI call you make yourself, or an execute
you run. A triggered run that tries one fails with
`Capability "<name>" needs a live person`, naming the capability it tried.
Everything else runs unattended with the enabler's live permissions.

Site-admin tools run unattended too when the person the run acts for is a site
admin: a job, webhook, subscription, inbound email handler, or `jobRunNow` in
that person's personal org or any team org they enabled (or clicked run-now in)
may call them, and every such call is audited with its trigger type and id.

Publishing a package never turns anything on, and nobody can enable a trigger
for someone else. `enablementList` shows your enablements (an Owner can list the
whole org's). `enablementDisable` turns one off; its enabler or an Owner may do
that.

Every time a job or an enablement turns on or off, the org audit log records who
did it (a person, an automation run, or a site admin), when, the old and new
state, where it came from (web app, MCP, API, CLI, the scheduler, and so on),
and the reason when one was given (`jobUpdate` and `enablementDisable` take an
optional `reason`). Owners see the last 7 days under **On/off changes** on the
Activity page. A job that reaches its own expiry time turns off without a row,
and pause and resume are a separate state with their own reason.

Example: Bea is a Member of `@acme` with Use on `@acme/digest`. She turns on its
nightly job. Each night it runs as Bea, with Bea's grants. It cannot read a
secret Bea has no Use on, and it cannot change roles even if Bea becomes an
Owner. If an Owner removes Bea from `@acme`, the job pauses until someone else
turns it on for themselves.

## Execute limits: one org-wide pool

Hosted execute limits (unique execute workers per day and per week) are one pool
per org, not per person:

- **Free**: one pool for the whole org (see the table below).
- **Pro**: the pool is the per-seat include times paid seats, so it grows with
  every Owner and Member. Funded credits carry it further, up to a ceiling that
  also scales with seats.
- The runaway brake (execute calls started per minute) is also per org: 300 for
  each Owner and Member, at least 300, on every plan.

A connection draws from the pool of the org it is bound to:

- A **Member** connected to `@acme` draws from acme's pool and is one of acme's
  seats.
- A **grant-only collaborator** connected to the owning org draws from that
  org's pool too, but is not a seat and does not grow the pool.
- Work in a person's personal org draws from that personal org's own pool.

Example: Jesse holds Use on `@acme/bot` and is not a member. Connected as
`@acme`, his runs count against acme's execute pool, credits, and budgets.
Nothing is charged to Jesse's personal plan, and acme's seat count does not
change.

Visits to an internal **package app** are billed differently: a visitor from
outside the org pays for the metered work their visits cause from their own
personal org, and the owning org keeps paying the app's Worker compute.
Internet-visible apps are always paid by the owning org. See
[Package apps: who pays for visits](/docs/package-apps#who-pays-for-visits).

## Connect one client to two orgs

A connection binds exactly one org at consent
([ADR 0064](https://raw.githubusercontent.com/kody-hq/kody/main/docs/contributing/decisions/0064-oauth-org-binding.md)).
To use two orgs from the same client, add Kody twice, once per org, with
`?org=<slug>` on the MCP URL:

```text
https://kody.codes/mcp?org=ada
https://kody.codes/mcp?org=ada-work
```

Each entry runs its own consent with that org selected, gets its own grant, and
acts only in that org. The slug must be an org you can access, and a slug that
disagrees with the one picked at consent is refused. Without `?org=`, consent
picks your only org or asks you to choose. For the CLI, use
`kody login --org <slug>`.

## Free team org limits

A new team org starts on the **same Free plan** as a personal org
([/pricing](/pricing)). There is no separate free-team SKU.

| Limit                                   | Free                                                                                      |
| --------------------------------------- | ----------------------------------------------------------------------------------------- |
| Free orgs you **own** (personal counts) | **2**, so one free team org beside personal                                               |
| Seats                                   | No seat charge on Free. Paid Pro seats are Owners + Members only ($12/seat/mo or $120/yr) |
| Saved packages                          | 10                                                                                        |
| Repos                                   | 20                                                                                        |
| Scheduled jobs                          | 5 (fastest interval 15 minutes)                                                           |
| Execute (unique workers) / day · week   | 150 · 400                                                                                 |
| Outbound fetches / day · week           | 1,000 · 2,500                                                                             |
| Job runs / day                          | 500                                                                                       |
| Automation invocations / day            | 1,000                                                                                     |
| Secrets                                 | 25                                                                                        |
| Storage                                 | 16 MiB                                                                                    |
| Concurrent workflows                    | 1                                                                                         |
| Active repo sessions                    | 5                                                                                         |

Credits: Free has no prepaid debit wallet. New accounts get a $5 welcome grant
held until the account is **credit-eligible Pro** (purchasable Pro subscription,
or admin credit eligibility on an effective Pro plan; not every paid price is
eligible). Until then the balance is held and not spent.

Example: Ada owns `@ada` (personal, Free) and creates `@ada-work` (team, Free).
That uses both free-org slots. A third free org she tries to own is refused
until one is paid or deleted.

## Paid seats

When an org is Pro, each live Owner and Member is a seat, and each seat grows
the org's execute pool. Billing-only members and outside collaborators are not
seats. See [Share a package](/docs/package-sharing) for grants vs membership.

## Move a Pro plan

An Owner of two orgs can move unused prepaid Pro value from one org to another
they own: **Usage and billing** → **Move this plan to another organization**
(`/@<slug>/-/billing/move-plan`). Packages, credentials, members, and wallets
stay put. The source org drops to Free limits. Only a live Owner of both orgs
can start, retry, or cancel the move. Billing admins cannot. A Free target buys
Pro at Stripe Checkout first; a target that already has Pro only receives the
credit. Retired prices, canceling subscriptions, and mismatched currencies are
refused.
