Outcome
Promote a scoped user to an organization member when the caller can manage the organization or target application grants.
These examples prove local handler behavior with real isolated SQL and explicitly simulated identity/permission dependencies.
Before you start
-
Verify identity and the current credential-scoped action contract before preparing input. Resource-grant operations authorize the target organization/app at runtime; metadata discovery does not prove execution permission.
-
Discover the recipient and existing application resource. Use one exact organization/app/type/id tuple consistently; a grant never creates the target resource.
-
Review the validated plan and intended effect before confirmation. Capture previous grant settings before replacing/updating/revoking and all organization grants/membership before promotion.
Discover the target
topolo whoami --json
topolo apps --json
topolo actions get app_topolo_auth.resource_grants.promote --detail --json
Discover the recipient and target resource using owning-application actions. Read app_topolo_auth.resource_grants.list_resource with the exact resource tuple and follow all pages.
Input fields
| Field | Required | Meaning |
|---|---|---|
userId | Yes | Existing recipient identity id from authorized discovery. On create it takes precedence over email. Default provisioning policy does not independently require that a supplied id exists; use existing_user when account existence must be enforced. |
organizationId | Yes | Exact organization whose resource access is being managed. Authority is checked against this organization and application, not merely the active client context. |
role | No | Requested organization member role, default member. This is broad organization membership, not a resource scope. The response echoes the requested role; verify actual membership and effective permissions separately, especially for an existing member. |
appId | No | Application identifier as stored on the grant. Permission lookup can resolve an app slug, but stored grants and listing filters use the supplied value unchanged. Use one discovered canonical id consistently. On promote this selects the app permission check; promotion still affects organization membership and grants across applications. |
Effects
-
Creates broad organization membership and revokes all this user non-workspace resource grants in the organization across applications.
-
Membership writes precede grant revocation; notification delivery is nonfatal.
Interpret the response
The raw target success envelope is success/message/data/timestamp. Create returns HTTP 201; all other successes return 200. CLI and MCP may wrap or project this envelope.
Grant id and createdAt remain stable when create updates the same user/organization/app/resourceType/resourceId key or reactivates a revoked grant. Omitted scope/type/metadata/expiry reset to create defaults rather than preserve previous values. Take a snapshot before replacing an existing grant.
Live lists omit revoked and expired rows. page.hasMore controls continuation; nextCursor is null at the end. list_mine contains only the authenticated subject, never a supplied userId. list_resource additionally returns recipient id/email/name or null when identity lookup is unavailable.
Create data.provisioned means this call created an unverified identity. It does not imply membership, invitation, sign-in, recipient acceptance or resource access. A create with past expiry can succeed with data.grant null because its stored row is already absent from live access.
Grant createdAt, expiresAt and revokedAt are epoch seconds; response timestamp is ISO text. Legacy create expiry strings can remain strings in stored output. Convert dates before planning; never infer live access from a successful storage response.
Update preserves identity and can extend an expired unrevoked grant. A revoked row is refused. Revoke returns only data.id, including on repeated revocation, and does not delete the recipient identity or affect separate membership/other grants.
Promote creates organization membership before revoking all non-workspace resource grants for that user and organization across apps. appId limits the permission check, not the revocation scope. These writes span databases and are not atomic; a 500 can leave membership active while grants remain. Read membership/effective access and remaining grants before any reviewed retry or compensation.
Promote data.role echoes the requested/default role. Existing membership, stored user role and effective authorization require separate verification. Notification delivery is nonfatal and is not proven by success. An empty list_mine after promotion does not mean the user has lost organization access.
AUTH_ERROR (401) is used both for absent/invalid tokens and for authenticated callers lacking organization or app grant authority. Inspect the error and identity before assuming OAuth expired; preserve authorization denials rather than changing credential context automatically.
Worked examples
These are complete raw target-handler responses, not CLI/gateway envelopes or live authorization proof. Replace synthetic identifiers using authorized discovery. Save only the reviewed input as payload.json; inspect validation and plan before executing. --confirm assumes the intended operation is approved. Metadata read-back requires separate recipient-principal and owning-application access tests.
Reviewed retry finishes revocation across applications after partial promotion
Input · payload.json:
{
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people"
}
CLI:
topolo actions validate app_topolo_auth.resource_grants.promote --data @payload.json --json
topolo actions plan app_topolo_auth.resource_grants.promote --data @payload.json --confirm --json
topolo actions call app_topolo_auth.resource_grants.promote --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.resource_grants.promote",
"input": {
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people"
},
"confirm": true
}
}
Captured local handler response · HTTP 200:
{
"success": true,
"message": "User promoted to member",
"data": {
"userId": "usr_recipient",
"organizationId": "org_example",
"role": "member"
},
"timestamp": "2026-10-02T18:01:40.152Z"
}
Repeated manager promotion preserves one membership and does not restore revoked grants
Input · payload.json:
{
"userId": "usr_recipient",
"organizationId": "org_example",
"role": "member"
}
CLI:
topolo actions validate app_topolo_auth.resource_grants.promote --data @payload.json --json
topolo actions plan app_topolo_auth.resource_grants.promote --data @payload.json --confirm --json
topolo actions call app_topolo_auth.resource_grants.promote --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.resource_grants.promote",
"input": {
"userId": "usr_recipient",
"organizationId": "org_example",
"role": "member"
},
"confirm": true
}
}
Captured local handler response · HTTP 200:
{
"success": true,
"message": "User promoted to member",
"data": {
"userId": "usr_recipient",
"organizationId": "org_example",
"role": "member"
},
"timestamp": "2026-10-02T18:01:40.153Z"
}
Verify the result
-
Discover and read authorized organization_users.list/users.get and user_permissions.get for the intended recipient, checking actual active membership, stored role and effective permissions rather than the echoed role.
-
As the intended recipient, list_mine with org_id and follow all pages across apps. Confirm no scoped non-workspace grants remain, then verify the intended organization access in its owning application.
-
After a failure, verify both membership and remaining grants before planning recovery. Success and empty grants do not prove notification delivery or downstream access.
Errors and recovery
Bare recipient cannot promote into an organization
Captured local refusal · HTTP 401:
{
"input": {
"userId": "usr_recipient",
"organizationId": "org_example"
},
"response": {
"success": false,
"error": {
"message": "Not authorized to manage this organization",
"code": "AUTH_ERROR",
"details": null,
"timestamp": "2026-10-02T18:01:40.151Z"
}
}
}
Application operator needs appId for permission-based promotion
Captured local refusal · HTTP 401:
{
"input": {
"userId": "usr_recipient",
"organizationId": "org_example"
},
"response": {
"success": false,
"error": {
"message": "Not authorized to manage this organization",
"code": "AUTH_ERROR",
"details": null,
"timestamp": "2026-10-02T18:01:40.151Z"
}
}
}
Partial promotion can return 500 after membership has already been created
Captured local refusal · HTTP 500:
{
"input": {
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people"
},
"response": {
"success": false,
"error": {
"message": "Fixture failure after membership before grant revocation",
"code": "SERVER_ERROR",
"details": null,
"timestamp": "2026-10-02T18:01:40.152Z"
}
}
}
Published recovery guidance
-
state_conflict(HTTP 409): Read the current resource state, reconcile the intended change, and retry only if still valid. -
VALIDATION_ERROR(HTTP 400): Correct reported fields. Pair resource filters, preserve pagination cursors and use workspace access actions for workspace grants. -
AUTH_ERROR(HTTP 401): Inspect identity and error message: invalid token and insufficient grant authority both use 401. Refresh an expired token when necessary; obtain intended access through its authorized owner for permission denials. -
NOT_FOUND(HTTP 404): Rediscover the exact live resource grant. Revoked rows cannot be updated; creation reactivation is a separate reviewed access grant. -
SERVER_ERROR(HTTP 500): Inspect actual stored state before retrying. Create can fail after identity provisioning; promote can fail after membership creation. Correct unsupported SQL-constrained scope/type input; do not treat a failure as proof that nothing changed.
Recovery boundaries
There is no atomic inverse in this action family. Read actual membership/effective access and all remaining organization grants after an unknown or partial outcome. Review finishing promotion only if broad membership remains intended. Removal of membership and restoration of captured grants require separate authorized owning actions and fresh verification; never assume removing one grant reverses promotion.
Discovery and evidence
This record belongs to app_topolo_auth. Local tests prove real resource-grant SQL and handler responses with simulated identity, permissions, identity lookup/provisioning and stored-role synchronization. They do not prove recipient sign-in, invitation delivery, owning-application resource enforcement, live permissions, deployment or production completion.