Files
windmill/docs/workspace-roles.md
T
hugocasaandClaude Opus 5.5 e5dedd147e feat: let flow builder operators build AI agents (#11898)
* feat: let flow builder operators build AI agents

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: check linked agents' bound tools and type, validate on the write's transaction

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: bound the linked-agent walk by sorting bound tool ids

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: let builders bind only the agent tool inputs left to the model

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: check every agent tool sharing a bound id

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: show builders the agent's own tool inputs read-only and name agents in the setting

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: show the agent's own tool inputs with the read-only step viewer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style: give the agent-set inputs heading the input label style

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style: label the agent's fixed tool inputs static inputs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: address cubic review on agent builder rights

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: check agent writes against the role's flows capability

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: name the flows capability as the agent builder right

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: gate agent evals on the role's code capability

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: move the composed-flow check into windmill-api-auth

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:05:20 +02:00

4.2 KiB

Workspace roles

What a non-admin member may author, manage and see is its role. developer and operator are the default roles; a licensed EE instance defines its own list in the workspace_roles global setting (at least one role), and any workspace may override a role's capabilities in workspace_settings.role_overrides, or, when licensed, add roles of its own there. A lapsed license freezes them rather than dropping them (custom_roles_enabled): the stored list and the workspace-only roles keep applying, writes that change them are refused, and resetting the list to the defaults or removing a workspace-only role stays open. Falling back to the defaults instead would hand a restricted developer everything. Workspace admin is not a role: it is the is_admin flag on top of whichever role a member holds, and holds everything. The code is windmill_common::roles.

Capabilities

Capability Values Enforced
code bool yes: scripts, previews, dependency jobs, dynamic selects, resolving jobs, data table migrations, eval datasets
flows none / compose / full yes; also AI agents, whose tools are checked as composed steps
apps none / compose / full yes; low-code apps and worker-built apps need full
manage_schedules, manage_triggers bool yes, see docs/operator-write-rights.md
visibility.* bool per page no, UI only

compose is the former operator builder right: the composition-only checks of docs/operator-builder-rights.md run for it and are skipped only at full. full flows or apps without code is refused at write, since both carry inline code.

Stored values are patches: an unset key falls through to the layer below, so a key added later reads as "never configured". The layers, top first: the workspace override, the instance definition, then RoleCapabilities::base_for (FULL for developer, the legacy operator for any other name). A role name no longer defined resolves to NONE, so deleting a role fails closed; deleting one that a member, invite, auto-invite or instance group mapping still names is refused, and so is deleting an instance role that a workspace still overrides (the override would fall onto base_for instead of the definition it was written against).

Who is restricted

ApiAuthed::is_operator means "this member's role is restricted", computed at authentication from the role, never from usr.operator alone. Every gate fast-paths on it (caller_capabilities returns FULL for an unrestricted or admin caller without reading the role), so it must stay derived from the role: a stale flag lets a restricted member through. The authentication cache holds it for up to 120s.

usr.operator is kept as "this role cannot write code", which is what clients that predate roles read it as. A member's role is usr.workspace_role, or, while that is NULL, operator / developer from usr.operator: rows written by paths that know nothing of roles stay meaningful. The old usr.role column is unrelated and holds legacy display strings.

Seats

A role that writes or composes anything costs a full seat; anything else half. Billing SQL cannot merge capability patches, so role_seat materializes the author flag per role (workspace_id = '' for the instance, a row per workspace override), rewritten by sync_role_seats after every write to either setting. Every count reads it through AUTHOR_SEAT_SQL, and usage telemetry reports builders (authors anywhere) against non-builders the same way. A change that adds seats under an offline license is priced inside the transaction that writes it, by check_seat_cap_for_role_change.

Legacy operator_settings

/operator_settings still works: it is applied as an override of the operator role, dropping any key equal to what the instance gives that role, and the payload is mirrored into the operator_settings column for git sync and older servers. The migration moved every key a workspace had changed from its default into role_overrides.operator the same way. The workspace list still returns an operator_settings object for a restricted member, synthesized from its role, alongside role and capabilities.