put_api_management_v1_workspaces_slug_state
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
#1152 — agent-profile runbooks, upserted per slug. The READ side of this
document has carried agent_profiles since #998; this is the write half, so
a provisioning run can seed the WHO of a launch (harness, model, effort,
allowlist tier, closeout/check gates) alongside the projects and credentials
it already seeds. Additive like every list here: a profile absent from the
payload is never deleted server-side. Requires
management:agent_profiles:write.
The desired state of ONE agent profile. Identified by the URL {key} (its
slug) on the item route, or by slug inside the declarative state document.
object
ADR 0045 §3.6 — default reasoning effort
(minimal | low | medium | high | max). Optional; preserves the current
value on update, leaves None (inherit the runner default) on create.
Issue #364 / ADR 0039 (D7); #708 who-3 — normal | workflow (ultracode
RETIRED to the effort axis). Optional; preserves the current value on update,
defaults to normal on create.
ADR 0045 §3.2 — opt this profile into the interactive tool-approval gate
(resolve_interactive_approvals_for_run). Optional; preserves the current
value on update, defaults to false on create. #708 W5 wired this WRITE
path (it was read-only before — the create/update never persisted it).
#838 — per-profile DEFAULT for the subagent concurrency ceiling (migration
358; 1..=64). The middle tier of task.config.max_subagents ?? profile ?? workspace ?? (no env). DOUBLE-Option with the same semantics as
[Self::max_turns]: absent preserves the current value on update / leaves
NULL on create; an explicit null clears it; a value sets it. “Absent =
unchanged” is load-bearing — the simple profiles tab sends a partial payload
and must never null a stored default (the #858 W4a ops-cap pattern).
#1018 — per-profile DEFAULT for the harness turn limit (migration 340,
positive). DOUBLE-Option: absent preserves the current value on update / leaves
NULL (no default — the runner’s 500 applies) on create; an explicit null
clears it; a value sets it. “Absent = unchanged” is load-bearing — the
simple profiles tab sends a partial payload, and it must never null a
stored default (the #858 W4a ops-cap pattern).
ADR 0049 (#547 / #548) — opt-in gate for the agent memory.create MCP
tool. Optional; preserves the current value on update, defaults to false
(fail-closed) on create. A workspace admin sets this on the profile editor
to open the unreviewed direct-write path.
#858 W4a — per-profile override of the profile-hourly MCP Ops budget
(migration 320; range 1–1000). Same DOUBLE-Option semantics as
mcp_ops_per_task_cap. The workspace-daily Ops ceiling stays fixed.
#858 W4a — per-profile override of the per-task/per-tool MCP Ops budget
(migration 320; range 1–500). DOUBLE-Option: absent preserves the current
value on update / leaves NULL (the ADR 0028 default, 10) on create; an
explicit null clears the override back to the default; a value sets it.
Admin-gated — the profile PUT already requires workspace admin.
#1176 — the profile’s RESOURCE CLASS (migration 361; normal | heavy).
The middle tier of task.config.resource_class ?? profile ?? workspace ?? normal.
A class is the profile’s statement about how much MACHINE its work needs:
the container’s memory cap and, inseparably, how many of the workspace’s
concurrency slots the task holds. Declaring heavy therefore does not
raise the host’s peak footprint — it trades parallelism for room.
DOUBLE-Option with the same semantics as [Self::max_turns]: absent
preserves the current value on update / leaves NULL on create; an explicit
null clears it; a value sets it. “Absent = unchanged” is load-bearing —
the simple profiles tab sends a partial payload and must never null a
stored declaration.
The script-run allowlist. Optional; absent preserves the current value on update
/ leaves None (OPEN — every script allowed) on create; a value (incl. [],
fail-closed) sets it. Mirrors capability_allowlist. #708 W9 wired this so a
workspace manifest round-trips it — the manifest apply supplies a fail-closed
Some([]) default (never the open None) when the manifest omits it.
#1175 (migration 363) — the profile’s TOOL-GATE deny budget in minutes,
the middle tier of task.config.tool_approval_deny_minutes ?? profile ?? workspace ?? 1440.
It governs ONLY the deny timeout policy, which a caller must name
explicitly. hold — the default for every runner-raised gate since #1164 —
carries no deadline, and no value here can give it one.
Range [5, 43200], enforced by the migration-363 CHECK and refused BY NAME
at write time rather than clamped, so a stored value is always the value
that applies. DOUBLE-Option, same semantics as [Self::max_turns].
Promote this profile to the workspace default (true), clear the
workspace default entirely (false), or leave it untouched (absent).
#242/ADR 0033 — a task with no agent_profile_id resolves to the
workspace default, so this is the WHO of every unpinned launch.
object
Optional MCP + skill capability map, mirroring the workspace
manifest shape from ADR-0007. When present, the Management API
applies it via the same apply_capabilities path used by
POST /api/workspace/apply-manifest. Absent keys are no-ops —
existing callers that don’t send this field keep working
unchanged. See bw-infra docs/38 §4.6.
object
object
One MCP or skill override inside a manifest. Mirrors the schema
described in docs/schemas/supacloud-workspace.schema.json.
object
object
object
One MCP or skill override inside a manifest. Mirrors the schema
described in docs/schemas/supacloud-workspace.schema.json.
object
object
object
object
object
#1214 — official-catalog listings this workspace carries, by slug.
ADDITIVE like every list here, and here that is load-bearing rather than
conventional: there is no uninstall path at all, so a removed entry cannot
be honoured even in principle. Requires management:credentials:write —
an install materialises the listing’s resources into the workspace.
One OFFICIAL-catalog listing this workspace carries.
ADDITIVE, deliberately: a listing that disappears from the document is NOT uninstalled, because no uninstall path exists (neither route nor query, measured 2026-08-31). Saying so here is the point — a document that reads as a full desired state while only ever adding is the more dangerous shape.
object
#1333 — the OPTIONAL bundle members to materialise (ADR 0072 D7). Absent installs the required set. A mapping on an optional member’s binding hole is refused unless that member is selected here. Honoured on INSTALL only: an update never grows the selection, exactly as the marketplace service states for every other caller.
#1333 / ADR 0072 D8-Ausnahme — the resource bindings this listing is to CREATE or WIDEN, named by resource NAME so the document stays portable.
A present, NON-EMPTY list makes the apply a SUBMITTED grant (D8): it may
fill a workflow member’s ADR 0058 §7 binding hole and arm its write half.
Absent or empty stays STORED — the apply may only preserve bindings that
are already in force, never create or widen one. The asymmetry is
deliberate: an IaC template that renders an unset list as [] must not
retract the bindings a running install carries, so “empty” is read as
“send nothing”, not as an explicit “bind nothing”.
One resource binding a management document asks an install to CREATE or WIDEN.
The browser install body names a resource_id (a UUID minted per instance);
a portable document cannot carry one, for the very reason
[ManagementRepoSyncSpec::credential_resource] states one block up: ids apply
only to the instance they were minted against. This keeps the document in the
vocabulary it already uses — the NAME of a workspace resource declared in its
own resources block — and the apply resolves the name to this instance’s id.
object
The in-guest binding NAME this mapping supplies authority for, when it
fills a workflow member’s ADR 0058 §7 hole. None is the pre-#1313
per-kind mapping, which fills no hole.
The WRITE grant for binding_name. None and false are the same safe
answer: only true asks for write, and only where the requirement asks it
too (the effective allow_write is the AND of the two).
The requirement KIND this mapping answers (seafile_webdav,
postgresql, …) — the same token the manifest requirement carries.
The NAME of a workspace resource holding the authority — never a UUID. An unknown or ambiguous name is an ERROR, not a silent non-binding.
#1313 / ADR 0073 D2 — the postgresql schema a schema_per_workspace
listing’s migrations apply into, for a workspace that has no recorded
namespace row yet.
The catalog ships a neutral placeholder and the INSTALL REQUEST names the
real schema, exactly as ResourceMappings.schema does for the browser
wizard (ADR 0073 D2). Absent or empty is the pre-#1313 shape and changes
nothing: a listing that does not opt in keeps its manifest literal, and an
opted-in listing with no recorded row is refused by name rather than
guessed. The value is never defaulted to a fixed schema name — the wrong
schema would write one tenant’s data into another’s.
skip_serializing_if matches ResourceMappings.schema: an absent schema
is “no opinion”, not an explicit erasure.
The catalog slug (scan-capture, finance-overview-app, …), resolved
against the official source. Same reason as credential_resource above:
item ids are per-instance, slugs are not.
Pin to one version. Absent means “track the newest installable version”, which is what a provisioning run wants — re-applying then carries a catalog update into the workspace.
object
object
object
#708 X2b — the PR-flow arming fields the management apply honors. auto_developer_enabled
arms the pr_review automation arm; auto_developer_agent_type stamps its
source_filter.harness. The legacy auto_developer_prompt/_modes/_draft_first/… spec
fields were removed with the dropped projects.auto_developer_* columns.
#708 M1/X5 — DEPRECATED. The standalone issue-debate lane was retired (#708 B3/X5): the
council capability is the per-stage council flag, and the auto_issue_debate_* columns
were dropped (migration 302). This field is KEPT only so a stale IaC manifest that tries
to ARM the lane fails LOUDLY: an apply that sets it true is REJECTED with a 422 (see
apply_project) sourced from THIS request field. false / absent stays accepted for
backward-compatible manifests. The other auto_issue_debate_* spec fields were removed.
#1161 D2 — the project’s AUTOMATION ARMS, reaching parity with the browser project
PUT’s automations block. This is the config home of review_enabled (the forensic
audit tail, mig 231) and pipeline_config (the return-to-sender loop’s
max_rounds / max_cost_usd ceilings + the per-stage overrides) — both of which the
management apply previously had NO door for, so an IaC caller could set a cost ceiling,
receive HTTP 200 and run with no ceiling at all.
ONE SOURCE, not a second truth (the #1152 rule for agent profiles applies verbatim):
each entry is the very SaveAutomationInput the browser PUT deserializes, applied
through the very project_automations::upsert_automation the browser calls — so the
Class-B PATCH-merge, the kind validation, the autonomy clamp and the workflow
RECONCILE (which materializes the arm’s template) are shared, not re-implemented.
Absent / null leaves every arm untouched.
What a save supplies for one automation. webhook_secret = None keeps any
stored secret on update (the persistence COALESCE) — a config edit need not
re-type it. autonomy_override is the REQUESTED level; the service clamps it
to the workspace ceiling before persist.
object
#534 WS-2 — the multi-agent work-lane candidate pool (backlog/simple lane).
Some(None) clears; Some(Some([])) = the All wildcard (every keyed
workspace profile, dynamic); Some(Some([ids])) = an explicit set; an omitted
key (outer None) keeps the stored pool.
#538 feinschliff — the dev-pipeline knobs JSONB (return_to_sender, convergence_level, max_rounds, max_cost_usd, per-stage overrides; mig 231).
#708 C1 — the two ADDITIVE completion booleans (post the closing comment × close/
transition the source issue). An omitted key keeps the stored value; when set, the write
derives + dual-writes completion_action (lossy for “silent close”).
#538 feinschliff — the Review toggle (the gated audit tail).
Write-only HMAC secret; an omitted/empty value keeps the stored secret (the persistence COALESCE), a non-empty value overwrites it.
#794 Stretch — selecting an existing workflow (including a Marketplace
install) binds it directly to this arm. null restores the platform
managed template only when the arm was previously selected; omitted keeps
the current binding.
#708 M1 (K-06) — the project-tier tiered standard-closeout override (mig 283),
reaching parity with the browser project PUT so an IaC apply can set it. A sparse
JSONB bag documented by the CloseoutConfigPatch schema; on apply it is
shape-validated (models::closeout::validate_patch) then merged into the applied
project config exactly like the browser PUT (persisted via
queries::projects::set_closeout_config). null / absent leaves the column
untouched (the project inherits the workspace default). The management owner token
acts as the workspace owner, so no additional per-field gate applies.
object
Gates the Tier-0 closeout MEMORY proposal. None inherits the next tier.
#708 W13 (M2M parity) — the project-tier SPARSE memory-governance override (mig 289),
reaching parity with the browser project PUT so an IaC apply can set it. A sparse JSONB
bag documented by the ProjectMemoryGovernanceOverride schema; on apply it is routed
through the very sc-svc-projects apply_memory_governance_policy service the browser
PUT calls (shape-validated, then persisted via queries::projects::set_memory_governance_policy),
so the session door and the M2M door share ONE validation + write path rather than a
parallel validate+setter pair. null / absent leaves the column untouched (the project
keeps inheriting the workspace policy); there is no reset-to-NULL through this door,
exactly like the closeout_config sibling above.
object
The governance rule for ONE memory scope: whether new memories of that scope skip
the review queue (auto_publish), whether they must be human-reviewed
(require_review), and how long they are retained before the decay sweep may
archive them (retention_days, 0 = keep forever).
auto_publish and require_review are intentionally NOT collapsed into one flag:
the conservative default is “review required, not auto-published”, and a future
“auto-published BUT still logged for review” mode keeps both axes meaningful. The
validation below rejects the contradictory auto_publish && require_review combo.
object
New memories of this scope skip the review queue and land live immediately.
New memories of this scope must be human-reviewed before they go live.
Days a memory of this scope is retained before the decay sweep may archive it;
0 keeps it forever. Defaults per scope: shared scopes (workspace/project)
start at [DEFAULT_SHARED_RETENTION_DAYS] (#1228), personal at 0.
The governance rule for ONE memory scope: whether new memories of that scope skip
the review queue (auto_publish), whether they must be human-reviewed
(require_review), and how long they are retained before the decay sweep may
archive them (retention_days, 0 = keep forever).
auto_publish and require_review are intentionally NOT collapsed into one flag:
the conservative default is “review required, not auto-published”, and a future
“auto-published BUT still logged for review” mode keeps both axes meaningful. The
validation below rejects the contradictory auto_publish && require_review combo.
object
New memories of this scope skip the review queue and land live immediately.
New memories of this scope must be human-reviewed before they go live.
Days a memory of this scope is retained before the decay sweep may archive it;
0 keeps it forever. Defaults per scope: shared scopes (workspace/project)
start at [DEFAULT_SHARED_RETENTION_DAYS] (#1228), personal at 0.
The governance rule for ONE memory scope: whether new memories of that scope skip
the review queue (auto_publish), whether they must be human-reviewed
(require_review), and how long they are retained before the decay sweep may
archive them (retention_days, 0 = keep forever).
auto_publish and require_review are intentionally NOT collapsed into one flag:
the conservative default is “review required, not auto-published”, and a future
“auto-published BUT still logged for review” mode keeps both axes meaningful. The
validation below rejects the contradictory auto_publish && require_review combo.
object
New memories of this scope skip the review queue and land live immediately.
New memories of this scope must be human-reviewed before they go live.
Days a memory of this scope is retained before the decay sweep may archive it;
0 keeps it forever. Defaults per scope: shared scopes (workspace/project)
start at [DEFAULT_SHARED_RETENTION_DAYS] (#1228), personal at 0.
#1161 D2 — MISPLACED-LEVEL TRAPS, kept for the same reason
auto_issue_debate_enabled is kept: so a manifest that puts them here fails LOUDLY
instead of being answered 200 and thrown away. Both belong to ONE AUTOMATION ARM,
not to the project, so they live inside automations[] — a project-level
pipeline_config cannot even say WHICH arm’s ceilings it means. They were the
measured defect: a caller set max_cost_usd here, got HTTP 200, and ran with no
ceiling. A PRESENT value — true, false or an object — is rejected with a 422
naming the field and the place it moved to.
An explicit JSON null is NOT rejected: serde maps null onto None for any
Option<T>, so it is indistinguishable here from an omitted key, exactly as the
closeout_config sibling above documents for itself. That is deliberate rather
than merely accepted — null carries no configuration intent (it cannot express a
cost ceiling), so it is not the failure this trap exists to catch, and IaC
templates that render an unset variable as null keep working. Pinned by
management_project_apply_treats_a_null_arm_field_as_absent.
#1214 — the workspace’s git binding. Absent leaves any existing binding
untouched; present upserts it through the same service the browser PUT
calls. Requires management:credentials:write — a repo-only binding
rewrites the whole workspace from the repo, resources and SOPS-imported
secrets included.
object
ADR 0030 UX2-F debounced background push. Absent keeps the stored value,
false on creation.
Defaults to main in the service when empty or absent.
The NAME of a workspace resource holding the git credential — never a
UUID. Resource ids are minted per instance, so a document pinned to one
applies only to the instance it was written against; the document already
names resources this way in its own resources block.
An unknown name is an ERROR, not a silent unbinding: a typo’d credential would otherwise leave a private repo bound and every later pull failing for a reason nothing states.
ADR 0023 §3: bidirectional | repo-only | ui-only. Absent keeps the
stored value on an existing binding, and falls back to bidirectional
(the DB default) when creating one.
#918 — workspace resources (postgresql / seafile_webdav / telegram_bot / custom / …) with write-only secrets. Additive like every list here: entries absent from the payload are never deleted server-side.
#918 — one workspace resource in the desired state. Mirrors the resource
create/update API surface; the secret is WRITE-ONLY (never echoed back;
the apply result carries a fingerprint at most).
object
Resource kind (closed catalogue + custom, validated by the resources
service). Immutable on update, like the user-facing API.
Resource name — the upsert identity within the workspace (bindings and scripts reference resources by name).
Write-only secret. Absent/null → leave the stored secret alone
(additive contract — IaC can re-apply without re-supplying secrets).
A string is stored as-is; a JSON object (#914 custom structured
secrets) is stored as its canonical JSON serialisation.
object
once | daily | weekly | monthly | interval.
#908 — the step width of an interval cadence in minutes (5–1440).
Required iff cadence = 'interval'; rejected on any other cadence.
Optional task-scope configuration (5-stage MCP allowlist
precedence per ADR-0007). Absent → no scope write. Empty
TaskScopeConfig::default() → wipes all scoping back to “all
enabled MCPs available to every task”.
object
object
object
object
#708 S1 (K-05) — an optional TYPED per-run standard-closeout override, merged into
config.closeout (the exact key the launch path’s resolve_closeout_for_task reads)
by [Self::effective_config], so a M2M-launched task can set its closeout the same way
the web run-start override does. A sparse JSONB bag documented by the
CloseoutConfigPatch schema; null / absent inherits the project/workspace default.
object
Gates the Tier-0 closeout MEMORY proposal. None inherits the next tier.
object
Optional issue-link intent. When BOTH issue_id and issue_url are
present, the launch path links the task to that existing issue by
merging link_issue_id/link_issue_url into config via the typed
services::tasks::IssueLinkConfig builder (ADR 0032, Issue #248). Either
field alone is a no-op, keeping legacy callers that only send config
unchanged.
object
#918 — workflow schedules: one spec creates/updates the schedule row
AND its linked schedule-kind trigger on the workflow.
#918 — one workflow schedule in the desired state: upserts the schedule row
(cadence incl. interval per #908) and ensures the linked schedule-kind
trigger on the workflow exists and is enabled.
object
once | daily | weekly | monthly | interval.
Required iff cadence = 'interval' (5–1440 minutes, #908).
Stable reconcile identity (task_schedules.management_key).
First-fire hint — preserved across re-applies unless the cadence changes (same semantics as managed task schedules).
The workflow’s name (repo-sync slug / display name).
#987 Stage B — workflows MATERIALIZED FROM A SHIPPED TEMPLATE, upserted per
(project, key) via the workflows.management_key column (migration 337).
The blocks above can only RESOLVE an existing workflow by name; this one
CREATES it, which is what makes a mail / scan workspace provisionable in one
declarative request.
#987 Stage B — one workflow MATERIALIZED FROM A SHIPPED TEMPLATE in the desired state.
This is the block that makes a mail / scan workspace provisionable through the
product path: before it, the state-apply could only RESOLVE an already-existing
workflow by name (workflow_triggers / workflow_schedules), never create one,
so an operator had to hand-compose the graph in the builder or via repo-sync
first. Reconciled per (project, key) on workflows.management_key
(migration 337) — deliberately NOT on the workflow NAME, which cannot survive a
rename and is ambiguous across a workspace.
Additive like every list in the document: an entry that disappears from the
payload is never deleted server-side, and a re-apply of an entry whose workflow
already exists is a no-op reporting changed: false (the graph belongs to the
user once it is materialized — the apply never overwrites their edits).
object
The template’s ${{ config.* }} substitution map, plus the per-slug
materialization parameters (e.g. mail_intake’s mailbox_resource_id).
Missing keys fall back to the template’s registry defaults.
A value of the form ${{ resources.<name>.id }} is resolved against the
resources applied EARLIER IN THE SAME REQUEST, so an operator binds a
mailbox or a document library by NAME and never pastes a UUID into a
manifest. An unknown resource name fails the item loudly.
object
Stable reconcile identity within the project (workflows.management_key).
Optional display name for the materialized workflow. Absent → the
template’s own name:. Purely cosmetic: the reconcile identity is key,
so renaming here never orphans the row.
Project that owns the workflow (ManagementProjectSpec.key). Absent → the
workspace’s single project; ambiguous when the workspace has several.
The shipped template slug to materialize (mail_intake, scan_intake,
autodev, …). An unknown slug fails the apply LOUDLY with a 404 rather
than materializing nothing.
#918 — workflow triggers, upserted per (workflow, key) via the
management_key column (migration 325). repo-sync deliberately never
applies triggers: on pull — this is the declarative path.
#918 — one workflow trigger in the desired state, upserted per
(workflow, key) (migration 325 management_key).
object
Stable reconcile identity within the workflow.
Trigger kind (webhook, schedule, telegram, …) — validated by the
triggers service.
Project that owns the workflow (ManagementProjectSpec.key). Absent →
the workflow is looked up across the workspace’s landing project.
The workflow’s name (repo-sync slug / display name).
object
Responses
Section titled “Responses”Workspace desired state applied
object
object
Examplegenerated
{ "changed": true, "results": [ { "changed": true, "fingerprint": "example", "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "key": "example", "resource_type": "example" } ], "workspace_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"}