Topolo Docs Topolo Platform
Open workspace

Save an app action declaration

Create or update a reviewed action declaration with explicit visibility, schema and partial-failure handling.

Outcome

Create or update a reviewed action declaration with explicit visibility, schema and partial-failure handling.

POST /api/developer-console/apps/{appId}/actions

Before you start

Use the original organization with the published Developers apps:write grant and a separately discovered canonical Developers workspace at read_write access. Discover the exact accessible app record and current action id through supported reads; appId/actionId input is not a new permission.

The route requires an active, non-revoked/non-suspended developer context with onboardingState complete and app.canEdit. Owner/editor app access is required after the workspace match; a viewer or permission string alone does not authorize the mutation. The ordinary same-organization branch verifies the workspace with Auth. Its existing privileged cross-organization branch uses the seeded developer profile workspace, without honoring a requested workspace header; it does not bypass the app-workspace comparison.

Shared context can create a local identity mirror and initial developer profile for an enabled organization after platform workspace resolution, even before a later onboarding/app refusal. No Auth workspace is minted. Keep those possible initialization effects separate from the selected mutation.

Before a real mutation, read the complete declaration and publication state, review the exact before/after metadata and external effects, validate and plan against the current credential-scoped action, and obtain authorization for that concrete change. CLI --confirm or MCP confirm:true does not supply human authority.

Every example below is source-derived synthetic UNRUN. The fictional host, ids and timestamps grant no authority. No app action/declaration, manifest, Auth projection, provider, key or notification was created, updated, published or executed for these docs. The source pin is not proof of serving-runtime adoption.

Request and effects

Action: app_topolo_developers.apps.actions.create. It is a confirmed mutation; validate and plan the real discovered payload and resource context before authorization/execution.

Success is HTTP200 {success:true,message,data:{action:<complete mapped declaration>}}. The action mapper includes the schemas, agent contract, identity, normalized flags/status, sourceRevision and publication/error/timestamps. Its response is a saved declaration state, not an execution of that declared endpoint.

There is no expectedRevision, conditional hash or request idempotency key in these action inputs. Concurrent editors and lost responses require a fresh same-context read and before/after comparison. Preserve actual uncertainty and partial outcomes rather than blindly repeating a write or using another organization.

The endpoint resolves the original app’s validated canonical appId and its action partition. Permission normalization removes the matching app prefix, lowercases and replaces periods with colons. apiBaseUrl is parsed as HTTP(S) without URL credentials and reduced to its origin; an embedded URL path is not kept. The distinct action path remains the endpoint path.

Changing permission, authorizationMode, principalAccess, allowedServices, accessPolicy, agentAccess, confirmation, schema or target changes the declared execution/access contract. Those flags are not grants to the editor or proof the implementation enforces them. Review the real provider/handler and all current caller-specific authorization separately; never execute the declared action to verify registration.

Despite the action name create, this handler is an upsert by actionId. A new declaration defaults to draft when status is omitted; an existing declaration inherits its status. The published input also permits explicit status published. Because the partition immediately replaces its action projection with that stored status, an explicit published save or an edit of an already-published declaration can immediately change live catalog content. The blanket statement that all new declarations are staged is incomplete. Use explicit draft for an intended staged save and review any actual visibility change.

Input supports a complete flat declaration, an actionId-based partial update, or appId plus nested action. Use only keys admitted by the current published schema; source aliases are not permission to submit undeclared CLI/MCP fields. A new static action requires a nonempty permission after normalization. name is lowercased and must match ^[a-z][a-z0-9_.:-]{1,119}$; title is nonblank at most120, description nonblank at most1000, method is GET/POST/PUT/PATCH/DELETE, path begins /, and target is nonempty.

Nested input/output schemas and agent metadata are normalized as objects. A nonempty enriched agent contract needs HTTPS docsUrl, resource identity semantics, effects, errors with400–599 status/code/recovery, verification and useful root input/output schemas. Upload metadata has additional profile/field/type/bounds rules; inline-upload names require upload metadata. The source accepts an empty agent contract, which is not complete agent documentation. Source structural checks are not a full schema/example/handler semantic test.

Partial updates generally inherit omitted/nullish values. In particular null principalAccess falls back to existing principalAccess rather than reliably clearing it, and null schema/agentContract falls back to existing values. Do not infer that sending null removes a restriction or metadata. accessPolicy has its own presence-sensitive branch and rejects invalid policies/public-mode policies. Service mode needs allowedServices; service-capable principalAccess needs runtime mode plus a nonempty allowed-service list.

Absent booleans inherit the existing values; for a new declaration GET defaults readOnly, DELETE defaults destructive, and destructive/non-read writes default confirmation. Default agentAccess is off for destructive, auto for readOnly without confirmation, otherwise confirm. Explicit flags are metadata; they do not prove the endpoint has those effects.

The upsert writes the definition and replaces its searchable action row in one partition transaction, then changes the permission-generation marker. For a newly created id it increments the control app’s action_count in a separate database call. If that later count update fails, the declaration may already be committed. There is no all-store transaction or automatic counter repair, and another same-id upsert does not necessarily repair the missing increment.

sourceRevision is a generated rev_<actionId>_<seconds> marker for the saved declaration, not an immutable Git revision or compare-and-swap token. Existing createdAt, lastPublishedAt and lastPublishError are retained even if content/status changes. A retained published timestamp is not proof the latest declaration content has been synchronized to the application manifest/Auth. The create handler does not itself run the full application publisher.

Worked requests and responses

Save a complete new draft

Complete synthetic UNRUN HTTP200. No existing id, source clock2026-10-21T08:00Z, all checks and the separate counter update succeed. The supplied base URL path is removed; explicit draft prevents this example from requesting publication. Its contract and fixture host authorize no real operation.

Input:

{
  "appId": "app_docs_fixture",
  "name": "items.list",
  "title": "List fixture items",
  "description": "Return fixture items for illustration.",
  "method": "GET",
  "path": "/api/items",
  "target": "api",
  "apiBaseUrl": "https://fixture.invalid/ignored-path",
  "permission": "items:read",
  "authorizationMode": "static",
  "inputSchema": {
    "type": "object",
    "properties": {},
    "additionalProperties": false
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "items": {
        "type": "array",
        "items": {
          "type": "string"
        }
      }
    },
    "required": [
      "items"
    ],
    "additionalProperties": false
  },
  "agentContract": {
    "docsUrl": "https://fixture.invalid/docs/items",
    "resource": {
      "kind": "item",
      "idPath": "$.items[]"
    },
    "effects": [
      "Reads fixture item identifiers without changing items."
    ],
    "errors": [
      {
        "status": 403,
        "code": "permission_denied",
        "recovery": "Preserve the original scope and ask its legitimate owner to review the actual refusal."
      }
    ],
    "verify": [
      "Check items belongs to the selected original resource context."
    ]
  },
  "readOnly": true,
  "destructive": false,
  "requiresConfirmation": false,
  "agentAccess": "auto",
  "status": "draft"
}

Response:

{
  "success": true,
  "message": "App actions saved successfully",
  "data": {
    "action": {
      "actionId": "app_docs_fixture.items.list",
      "target": "api",
      "apiBaseUrl": "https://fixture.invalid",
      "appId": "app_docs_fixture",
      "developerId": "dev_docs_fixture",
      "organizationId": "org_docs_fixture",
      "name": "items.list",
      "title": "List fixture items",
      "description": "Return fixture items for illustration.",
      "method": "GET",
      "path": "/api/items",
      "permissionName": "items:read",
      "principalAccess": null,
      "authorizationMode": "static",
      "allowedServices": [],
      "inputSchema": {
        "type": "object",
        "properties": {},
        "additionalProperties": false
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "items"
        ],
        "additionalProperties": false
      },
      "agentContract": {
        "docsUrl": "https://fixture.invalid/docs/items",
        "resource": {
          "kind": "item",
          "idPath": "$.items[]"
        },
        "effects": [
          "Reads fixture item identifiers without changing items."
        ],
        "errors": [
          {
            "status": 403,
            "code": "permission_denied",
            "recovery": "Preserve the original scope and ask its legitimate owner to review the actual refusal."
          }
        ],
        "verify": [
          "Check items belongs to the selected original resource context."
        ]
      },
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "agentAccess": "auto",
      "status": "draft",
      "sourceRevision": "rev_app_docs_fixture.items.list_1792569600",
      "lastPublishedAt": null,
      "lastPublishError": null,
      "createdAt": "2026-10-21T08:00:00.000Z",
      "updatedAt": "2026-10-21T08:00:00.000Z"
    }
  }
}

Read back the exact draft declaration; no manifest publication or business execution is implied.

Edit an already-published declaration

Complete synthetic UNRUN HTTP200 assuming the existing row has the other shown fields, published status and the retained prior publication timestamp. Only description is supplied for update; status remains published and the catalog row can change immediately. This is not a staged draft edit.

Input:

{
  "appId": "app_docs_fixture",
  "actionId": "app_docs_fixture.items.list",
  "description": "Return the reviewed fixture item identifiers."
}

Response:

{
  "success": true,
  "message": "App actions saved successfully",
  "data": {
    "action": {
      "actionId": "app_docs_fixture.items.list",
      "target": "api",
      "apiBaseUrl": "https://fixture.invalid",
      "appId": "app_docs_fixture",
      "developerId": "dev_docs_fixture",
      "organizationId": "org_docs_fixture",
      "name": "items.list",
      "title": "List fixture items",
      "description": "Return the reviewed fixture item identifiers.",
      "method": "GET",
      "path": "/api/items",
      "permissionName": "items:read",
      "principalAccess": null,
      "authorizationMode": "static",
      "allowedServices": [],
      "inputSchema": {
        "type": "object",
        "properties": {},
        "additionalProperties": false
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "items"
        ],
        "additionalProperties": false
      },
      "agentContract": {
        "docsUrl": "https://fixture.invalid/docs/items",
        "resource": {
          "kind": "item",
          "idPath": "$.items[]"
        },
        "effects": [
          "Reads fixture item identifiers without changing items."
        ],
        "errors": [
          {
            "status": 403,
            "code": "permission_denied",
            "recovery": "Preserve the original scope and ask its legitimate owner to review the actual refusal."
          }
        ],
        "verify": [
          "Check items belongs to the selected original resource context."
        ]
      },
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "agentAccess": "auto",
      "status": "published",
      "sourceRevision": "rev_app_docs_fixture.items.list_1792569600",
      "lastPublishedAt": "2026-10-20T08:00:00.000Z",
      "lastPublishError": null,
      "createdAt": "2026-10-21T08:00:00.000Z",
      "updatedAt": "2026-10-21T08:00:00.000Z"
    }
  }
}

Verify actual current catalog contract and retain the old publication timestamp as historical state, not proof of new content synchronization.

Onboarding blocks the save

Complete synthetic UNRUN HTTP403 from the concrete onboarding check, before selected upsert. Shared context initialization may have occurred; no real denial was induced.

Input:

{
  "appId": "app_docs_fixture",
  "name": "items.list",
  "title": "List fixture items",
  "description": "Return fixture items for illustration.",
  "method": "GET",
  "path": "/api/items",
  "target": "api",
  "apiBaseUrl": "https://fixture.invalid/ignored-path",
  "permission": "items:read",
  "authorizationMode": "static",
  "inputSchema": {
    "type": "object",
    "properties": {},
    "additionalProperties": false
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "items": {
        "type": "array",
        "items": {
          "type": "string"
        }
      }
    },
    "required": [
      "items"
    ],
    "additionalProperties": false
  },
  "agentContract": {
    "docsUrl": "https://fixture.invalid/docs/items",
    "resource": {
      "kind": "item",
      "idPath": "$.items[]"
    },
    "effects": [
      "Reads fixture item identifiers without changing items."
    ],
    "errors": [
      {
        "status": 403,
        "code": "permission_denied",
        "recovery": "Preserve the original scope and ask its legitimate owner to review the actual refusal."
      }
    ],
    "verify": [
      "Check items belongs to the selected original resource context."
    ]
  },
  "readOnly": true,
  "destructive": false,
  "requiresConfirmation": false,
  "agentAccess": "auto",
  "status": "draft"
}

Response:

{
  "success": false,
  "error": {
    "message": "Complete onboarding before managing app actions."
  }
}

Preserve the actual original context and unmet onboarding condition; do not use a role/grant or alternate app to evade it.

Verify and recover

  1. Read all pages of apps.actions.list in the original app/workspace and locate the exact data.actions[].actionId. Compare every reviewed identity/target/access/schema/agent/status field, sourceRevision and retained publication metadata to the intended change. A later read sees current state, not a lock against another writer.

  2. For a planned staged save confirm actual status draft and the intended effective catalog visibility; for a preexisting published save inspect the current credential-scoped contract before any separate execution. Full manifest/Auth synchronization remains a separate authorized task.

  3. After an uncertain write, read before retrying and compare the complete preimage/intended state. Preserve a possible committed declaration with failed control counter update. Do not create a new id, delete records, grant access or assume retry is an exact inverse.

There is no automatic inverse or atomic cross-service rollback. An intentional corrective declaration or publication requires a fresh reviewed plan and authority for its actual effects. Never restore old flags/content or delete a declaration merely to conceal uncertain outcomes.

Change log

  • 2026-10-21: Added current-source mutation semantics, complete synthetic request/response examples, visibility and access distinctions, nontransactional outcome/recovery guidance and source-qualified status/error qualifications. Examples remain unrun.