Outcome
Grant a user access to one application resource 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.create --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
Supply userId or email; at least one recipient selector is required.
| Field | Required | Meaning |
|---|---|---|
userId | No | 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. |
email | No | Recipient email used only when userId is absent. The handler trims and lowercases it before identity lookup. Provisioning creates an unverified identity; this action sends no invitation or sign-in link. |
name | No | Optional display name for a newly provisioned identity; does not rename an existing recipient. |
organizationId | Yes | Exact organization whose resource access is being managed. Authority is checked against this organization and application, not merely the active client context. |
appId | Yes | 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. |
resourceType | Yes | Owning application resource kind; workspace is prohibited here and requires workspace_access.members.grant. This does not create or validate the application resource. |
resourceId | Yes | Existing target resource identifier from the owning application. Grant metadata alone does not prove the application recognizes or enforces this resource. |
scope | No | Use read, read_write or admin. Create defaults to read and its legacy string input is constrained by the SQL schema; unsupported values can cause SERVER_ERROR. Update explicitly validates these three values. The owning application must enforce the chosen scope. |
grantType | No | Use consumer, invited_member or exception; defaults to consumer. The legacy string input is constrained by the SQL schema. It is a grant label, not proof of organization membership. |
expiresAt | No | Epoch seconds or null for no expiry. Use a positive integer when setting expiry; past expiry removes live access. Create also accepts legacy strings, which are not reliably date-validated: convert timestamps to integer seconds before execution. Update accepts positive integer seconds or null only. |
metadata | No | Optional application-defined JSON object, replaced on repeated creation rather than merged. null clears metadata. Do not put credentials or confidential unrelated data in it. |
recipientPolicy | No | Defaults to provision_if_missing. existing_user requires an existing account. existing_user_with_app_access also requires assigned app access in any active organization membership, which need not be the grant organization. None of these policies delivers an invitation. |
Effects
-
Creates or reactivates one non-workspace resource grant; repeated natural key replaces scope/type/metadata/expiry and preserves id.
-
May provision an unverified consumer identity without delivering an invitation or creating membership.
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.
Organization manager grants existing recipient read access
Input · payload.json:
{
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"recipientPolicy": "existing_user"
}
CLI:
topolo actions validate app_topolo_auth.resource_grants.create --data @payload.json --json
topolo actions plan app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
topolo actions call app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.resource_grants.create",
"input": {
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"recipientPolicy": "existing_user"
},
"confirm": true
}
}
Captured local handler response · HTTP 201:
{
"success": true,
"message": "Resource grant created",
"data": {
"grant": {
"id": "rg_JOD6QZYnE1JA",
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"grantType": "consumer",
"scope": "read",
"expiresAt": null,
"grantedBy": "usr_manager",
"metadata": null,
"revokedAt": null,
"createdAt": 1790964100
},
"provisioned": false
},
"timestamp": "2026-10-02T18:01:40.143Z"
}
Repeated natural key updates scope and metadata while preserving id
Input · payload.json:
{
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"recipientPolicy": "existing_user",
"scope": "read_write",
"metadata": {
"purpose": "interview"
}
}
CLI:
topolo actions validate app_topolo_auth.resource_grants.create --data @payload.json --json
topolo actions plan app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
topolo actions call app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.resource_grants.create",
"input": {
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"recipientPolicy": "existing_user",
"scope": "read_write",
"metadata": {
"purpose": "interview"
}
},
"confirm": true
}
}
Captured local handler response · HTTP 201:
{
"success": true,
"message": "Resource grant created",
"data": {
"grant": {
"id": "rg_JOD6QZYnE1JA",
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"grantType": "consumer",
"scope": "read_write",
"expiresAt": null,
"grantedBy": "usr_operator",
"metadata": {
"purpose": "interview"
},
"revokedAt": null,
"createdAt": 1790964100
},
"provisioned": false
},
"timestamp": "2026-10-02T18:01:40.144Z"
}
Recipient app assignment in another active organization satisfies the policy
Input · payload.json:
{
"userId": "usr_second",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"recipientPolicy": "existing_user_with_app_access"
}
CLI:
topolo actions validate app_topolo_auth.resource_grants.create --data @payload.json --json
topolo actions plan app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
topolo actions call app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.resource_grants.create",
"input": {
"userId": "usr_second",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"recipientPolicy": "existing_user_with_app_access"
},
"confirm": true
}
}
Captured local handler response · HTTP 201:
{
"success": true,
"message": "Resource grant created",
"data": {
"grant": {
"id": "rg_6nDEPU6LsT4w",
"userId": "usr_second",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"grantType": "consumer",
"scope": "read",
"expiresAt": null,
"grantedBy": "usr_manager",
"metadata": null,
"revokedAt": null,
"createdAt": 1790964100
},
"provisioned": false
},
"timestamp": "2026-10-02T18:01:40.144Z"
}
Creation reactivates the same revoked natural key
Input · payload.json:
{
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"recipientPolicy": "existing_user"
}
CLI:
topolo actions validate app_topolo_auth.resource_grants.create --data @payload.json --json
topolo actions plan app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
topolo actions call app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.resource_grants.create",
"input": {
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"recipientPolicy": "existing_user"
},
"confirm": true
}
}
Captured local handler response · HTTP 201:
{
"success": true,
"message": "Resource grant created",
"data": {
"grant": {
"id": "rg_JOD6QZYnE1JA",
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"grantType": "consumer",
"scope": "read",
"expiresAt": null,
"grantedBy": "usr_manager",
"metadata": null,
"revokedAt": null,
"createdAt": 1790964100
},
"provisioned": false
},
"timestamp": "2026-10-02T18:01:40.149Z"
}
Past expiry is stored but create returns null grant
Input · payload.json:
{
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_expired",
"recipientPolicy": "existing_user",
"expiresAt": 1
}
CLI:
topolo actions validate app_topolo_auth.resource_grants.create --data @payload.json --json
topolo actions plan app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
topolo actions call app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.resource_grants.create",
"input": {
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_expired",
"recipientPolicy": "existing_user",
"expiresAt": 1
},
"confirm": true
}
}
Captured local handler response · HTTP 201:
{
"success": true,
"message": "Resource grant created",
"data": {
"grant": null,
"provisioned": false
},
"timestamp": "2026-10-02T18:01:40.149Z"
}
Default recipient policy provisions an unverified identity without membership or invitation
Input · payload.json:
{
"email": "new@example.test",
"name": "New recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_new"
}
CLI:
topolo actions validate app_topolo_auth.resource_grants.create --data @payload.json --json
topolo actions plan app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
topolo actions call app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.resource_grants.create",
"input": {
"email": "new@example.test",
"name": "New recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_new"
},
"confirm": true
}
}
Captured local handler response · HTTP 201:
{
"success": true,
"message": "Resource grant created",
"data": {
"grant": {
"id": "rg_Xz4YPdw0iSua",
"userId": "usr_provisioned_1",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_new",
"grantType": "consumer",
"scope": "read",
"expiresAt": null,
"grantedBy": "usr_manager",
"metadata": null,
"revokedAt": null,
"createdAt": 1790964100
},
"provisioned": true
},
"timestamp": "2026-10-02T18:01:40.149Z"
}
Separate application resource grant exists before promotion
Input · payload.json:
{
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_pay",
"resourceType": "invoice",
"resourceId": "invoice_one",
"recipientPolicy": "existing_user"
}
CLI:
topolo actions validate app_topolo_auth.resource_grants.create --data @payload.json --json
topolo actions plan app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
topolo actions call app_topolo_auth.resource_grants.create --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.resource_grants.create",
"input": {
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_pay",
"resourceType": "invoice",
"resourceId": "invoice_one",
"recipientPolicy": "existing_user"
},
"confirm": true
}
}
Captured local handler response · HTTP 201:
{
"success": true,
"message": "Resource grant created",
"data": {
"grant": {
"id": "rg_WQEhmLBVzLu5",
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_pay",
"resourceType": "invoice",
"resourceId": "invoice_one",
"grantType": "consumer",
"scope": "read",
"expiresAt": null,
"grantedBy": "usr_manager",
"metadata": null,
"revokedAt": null,
"createdAt": 1790964100
},
"provisioned": false
},
"timestamp": "2026-10-02T18:01:40.151Z"
}
Verify the result
-
Read list_resource for the exact organization/application/type/id and compare recipient, scope and expiry against the intended change; follow all pages.
-
Use the intended recipient principal with list_mine, then verify the owning application accepts only the intended resource/scope and denies an unrelated resource. Metadata read-back alone does not prove resource enforcement.
Errors and recovery
existing_user refuses unknown recipient
Captured local refusal · HTTP 400:
{
"input": {
"userId": "usr_missing",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"recipientPolicy": "existing_user"
},
"response": {
"success": false,
"error": {
"message": "Recipient must already have a Topolo account",
"code": "VALIDATION_ERROR",
"details": {
"userId": true
},
"timestamp": "2026-10-02T18:01:40.149Z"
}
}
}
Existing recipient without assigned app is refused
Captured local refusal · HTTP 400:
{
"input": {
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"recipientPolicy": "existing_user_with_app_access"
},
"response": {
"success": false,
"error": {
"message": "Recipient must have this application assigned",
"code": "VALIDATION_ERROR",
"details": {
"appId": true
},
"timestamp": "2026-10-02T18:01:40.149Z"
}
}
}
Unprivileged authenticated recipient cannot grant access
Captured local refusal · HTTP 401:
{
"input": {
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"recipientPolicy": "existing_user"
},
"response": {
"success": false,
"error": {
"message": "Not authorized to grant access to this resource",
"code": "AUTH_ERROR",
"details": null,
"timestamp": "2026-10-02T18:01:40.149Z"
}
}
}
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
For a newly created grant, review revoke with the returned id. For an existing key, restore its prior complete grant settings through a separately reviewed create rather than revoke valid previous access. Revocation does not delete a provisioned identity. If grant is null, no live access was returned; discover stored state before further changes.
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.