manage_proposals
This page answers how an AI builds in OpenX over MCP: what manage_proposals takes, how to check a
proposal before sending it, who accepts it, and every refusal it can answer with.
Purpose
manage_proposals creates, updates or deletes proposals by committing a partial of the workspace's
proposal groups. The same key updates a proposal and a new key creates one. It is the only tool that
writes, and it writes only proposals.
A person accepts them in the workspace portal. There is no accept tool over MCP, by design, so an agent connected from outside never accepts its own proposals. Any workspace member can accept a proposal; some changes (access, licences, connected-app requests) need a management right. Accepting writes the record to the workspace; it does not deploy it. Greenfield is Azi's own, inside the workspace: there, the proposals Azi makes are accepted for you, as you and with your rights, and never anyone else's.
The structure
A proposal sits at ProposalGroups[groupId].Details.Bundles[bundleId].Proposals[lookup]. Bundles
go inside Details, never beside it. Each proposal needs a Kind, a Lookup and a Proposed object. To
remove proposals, pass Deletes with null values; they are processed before the merge.
| Input | Meaning |
|---|---|
ProposalGroups | The groups to create or update, by group id |
Deletes | Proposals to take away, with null values |
checkOnly | true runs every check and writes nothing |
A proposal to remove a record has Intent: "delete" and an empty Proposed object, or only
Proposed: { Metadata: { Removal: { ... } } } with the record's delete settings. To place a new node on the
canvas, put its position at Proposed.Metadata.Position: { X, Y } (on a surface's canvas, the surface's own entry for
the node). No code places a node for you; lay it out by the convention on the Canvas page.
A check (and a submit) also answers layoutWarnings for the nodes it proposes (LAYOUT_OVERLAP, LAYOUT_LINES_CROSS,
LAYOUT_NO_POSITION), each with a freeSpot and a remedy, and otherLayoutWarnings, the count of warnings elsewhere
on the canvas. A warning never makes a check invalid and never blocks a submit.
A job identity proposal is alone in its bundle, and a person accepts it in the App Identity window.
A cloud connection and a landing zone
Clouds and CALZ are proposed like any other kind, and each has one record, keyed Workspace. Proposing is
not connecting or creating: a person accepts in the window and does their part there. In
Cloud Connections the proposal shows over the normal Connect form
(an existing subscription, or a new one under a billing scope), filled from it, and Accept proposal and
Connect accepts it and connects with what the form holds. In
Manage Private CALZ it is Accept Proposal and Create.
- A cloud connection proposal carries its name and description, and the subscription, tenant and billing scope
until the cloud is connected. They go in the cloud's own fields, as the codes
get_azure_listinggives, and stay changeable until the person connects, when their own Azure sign-in decides the real ids. The secret is refusedCREDENTIAL_IN_PROPOSAL, a connected cloud's subscription, tenant or billing scopeIDENTITY_ALREADY_STORED, a field only the person setsPERSON_ONLY_FIELD, a field the connection writes for itself (ID,AuthKeyID,AuthKeyExpires)ACTUATOR_OWNED_FIELD, and any other fieldUNKNOWN_CLOUD_FIELD. - A landing zone can be proposed once a cloud is connected. It carries its name and description, and its region
and resource group only while nothing is provisioned (else
CALZ_ALREADY_PROVISIONED). The region is the Azure slug, such aseastus2, never its display name (REGION_NOT_A_SLUG). A value the landing zone's creation writes, such asStorageAccountNameorKeyVaultName, is refusedACTUATOR_OWNED_FIELD, and any other fieldUNKNOWN_CALZ_FIELD. - The kinds that need the landing zone (Provenance, IoT, Data Stores, Communications, App Identity) wait until the
cloud is really connected and the landing zone created. Proposed before that, even in the same bundle as the
CloudsorCALZproposal, they are refusedPRIVATE_LANDING_ZONE_REQUIRED.
Check before sending
Call get_workspace with includeGuides: true for the kind's guide, schema and examples. Then send
the same proposal with checkOnly: true: it runs every check, and for a warm query also the api's own check, and
writes nothing. It answers { checked, valid, refusals }, each refusal with its remedy. checked: false means a
check could not run, so the proposal is not called valid.
Example
{
"checkOnly": true,
"ProposalGroups": {
"g1": {
"Details": {
"Bundles": {
"b1": {
"Proposals": {
"p1": {
"Kind": "DataConnections",
"Lookup": "boiler-4",
"Proposed": { "Details": { "Name": "boiler-4" } }
}
}
}
}
}
}
}
}
Without checkOnly, the answer is committed: true, the committed groupIds and the updated workspace (the
get_workspace body), so no second read is needed. If the proposals were committed but the read-back failed, the
answer is committed: true and the groupIds, with RefreshFailed and a Remedy.
Refusals
A malformed structure answers the issues found and how the structure must look. Every other refusal names the
kind, the lookup and, where it can, the field path, never a value, with a code, a message and a remedy.
The remedy is, in order: the refusal's own, the kind's own sentence for that code, the shared sentence below, or
a pointer to the kind's guide.
Refused before any check
| Code | Message |
|---|---|
REQUIRED | "Proposal must have Kind, Lookup, and Proposed fields" |
STATUS_NOT_AGENT_WRITABLE | "Status is set by acceptance in the workspace portal, never by a proposal. Omit the field." |
DELETE_LEAF_INVALID | "A removal carries no fields except its record's delete settings." Propose it with an empty Proposed, or with only Proposed.Metadata.Removal |
UNKNOWN_KIND | "Unknown proposal Kind '...'. Known kinds: ..." |
PROPOSED_DETAILS_METADATA | A position under Proposed.Details.Metadata, which is never drawn |
POSITION_NOT_DRAWN | A position on the proposal itself rather than Proposed.Metadata.Position |
MANAGE_PROPOSALS_WORKSPACE_UNREAD | "The workspace was not read for this sign-in, so no proposal was checked or submitted." |
Refused by the checks
| Code | What it means |
|---|---|
KIND_NOT_IN_PACK | "This kind is not turned on in this workspace's capabilities yet, so it cannot be proposed, changed or removed." The answer lists the kinds that are on, as ActiveKinds |
IDENTITY_NOT_CONFIGURED | No App Identity is set up, so access rights and access configurations cannot be proposed yet |
SINGLETON_PROPOSAL_EXISTS | The workspace holds one record of this kind and a proposal for it is already pending |
VALIDATOR_ERROR | The kind's checks could not complete; a fault on the server side, not the proposal |
CHECKS_UNAVAILABLE | "The workspace capability checks could not be loaded, so nothing was committed. Try again shortly." |
WARM_QUERY_CHECK_UNAVAILABLE | checkOnly only: the api's warm-query check could not run, so the proposal is not called valid |
SCHEMA_MISMATCH | "This field does not match the shape the workspace stores for this kind. Read proposal_schema for the field, then correct it." |
UNKNOWN_INTENT | A proposal either changes a record or removes one |
Refused removals
| Code | What it means |
|---|---|
KIND_NOT_DELETABLE | "Records of this kind are never removed from a workspace." |
REMOVER_NOT_ALLOWED | This kind cannot be removed from here; some kinds are a person's to remove |
KIND_TEARDOWN_UNSUPPORTED | Removing this kind is not built yet |
FROM_SURFACE_NOT_SUPPORTED | "This kind is removed as a whole record, not from one surface." |
DELETE_WITH_PROPOSED_FIELDS | A removal carries only its delete settings; propose any edit separately |
TARGET_IS_PROPOSAL_ONLY | Only a pending proposal describes this record; reject that proposal instead |
TARGET_NOT_FOUND | "There is no saved record by that name to remove." |
ALREADY_STAGED | "This record is already waiting to be removed on the next Deploy." |
RECORD_STAGED_FOR_DELETE | The record is waiting to be removed on the next Deploy, so it cannot be changed |
REFERENCED_BY | Something else still points at it; remove what is listed first, or remove them together |
SURFACE_NOT_EMPTY | The surface still holds things |
DATA_STORE_HAS_DATABASES | Databases still live in the data store |
DATABASE_HAS_TABLES | The database still holds containers |
DATABASE_STILL_READ | A warm query or a surface still reads the database |
ACTUATOR_NOT_REGISTERED | Nothing is registered to take this kind down in Azure, so the removal stops |
Each kind also has codes of its own; the kind's guide (from get_workspace with includeGuides: true) and the
proposal_schema resource carry them.
When the server fails
A commit the server could not make answers "Workspace commit failed (...). Nothing was committed. This is a fault on the server, not a problem with the proposal." Another failure answers "Managing proposals failed on the server. Nothing may have been committed; call get_workspace before retrying."