Custom actions — how an org composes its own
Custom actions are how an organization extends the Action Library with scopes for its own systems — approving a shipment, releasing a payment in an ERP, anything Axowl does not run itself.
Built with the same grammar
Section titled “Built with the same grammar”A custom scope is written exactly like a managed one. The only difference is the namespace: the first segment is the org’s own word instead of org, view, platform or user.
shipment.approve:region=KR,value<=50000└─ scope ──────┘ └──── constraints ────┘| Part | What it is | Example (a logistics org) |
|---|---|---|
| Namespace | the org’s system or domain — anything except org, view, platform, user | shipment |
| Action | what is done | approve |
| Constraints | the limits the permission carries | region=KR · value<=50000 |
Registering a custom scope
Section titled “Registering a custom scope”There is no separate “define” step. A custom scope is registered the first time it is added to a role:
POST /api/org/{slug}/roles/{roleId}/permissionsContent-Type: application/json
{ "templateExpression": "shipment.approve:region=KR,value<=50000" }- The caller needs
org.role.update(Owners and Admins have it implicitly). A caller who is not an Owner or Admin can add only a scope they hold themselves, constraints included; otherwise the call returns403witherror: "PERMISSION_DENIED". - The response is
{ "templateId": "…" }. Remove the entry again withDELETE /api/org/{slug}/roles/{roleId}/permissions/{templateId}. - The first time a scope is used, it is added to the catalog and an
auth.perm.createdevent is recorded. From then on it appears in the organization’s Action Library withorigin: "custom".
Composition rules
Section titled “Composition rules”- Shape. At least two dot-separated segments:
namespace.actionornamespace.resource.action. - Characters. Lowercase letters, digits,
-and_in each segment. - Wildcard.
*is allowed only as the last segment (shipment.*). - Constraints. After the colon, comma-separated entries of a key, an operator (
=,!=,<,<=,>,>=) and a value. Keys and values cannot be empty. A value can be a set —region={KR,JP}— and<,<=,>,>=take a number. - No lifecycle keys on a role.
uses,ttlandexpare refused with400anderrorCode: "LIFECYCLE_NOT_SUPPORTED". Set an expiry on the role assignment, or grant the scope through an approval form. - Reserved namespaces. A scope that starts with
org,view,platformorusermust be an existing Library scope. Anything else in those namespaces is refused with400anderrorCode: "SCOPE_NOT_IN_LIBRARY"— a made-uporg.*scope would be saved but never open anything.
Using a custom scope
Section titled “Using a custom scope”On a role. Members who hold the role hold the scope, with its constraints.
On a form. A custom scope can be the target of an approval form, like a managed one. The request is then approved along the approval line, under the same ceiling: an approver can only approve what they themselves hold. The form’s risk floor follows the scope’s last segment, as it does for managed scopes: shipment.delete starts at 3, shipment.approve at 2. A grant made this way can carry the lifecycle controls ttl / exp; a finite uses is not available on a custom scope and such a request is refused at submission (see Lifecycle control variables).
From the org’s system. Before performing the action, the org’s system asks Axowl whether it is within the rules:
POST /api/org/{slug}/permissions/checkContent-Type: application/json
{ "scope": "shipment.approve", "userId": "u-1042", "context": { "region": "KR", "value": 15000 }}{ "allow": true, "reason": "matched", "evaluationId": "0b0f…", "permissionId": "5c1e…", "ruleExpression": "region=kr,value<=50000"}How the answer is decided:
- The scope must be registered in this organization; otherwise
allowisfalsewith reasonno_matching_permission. - Every rule the organization’s roles attach to that scope is checked against
context. One violated rule denies the request, andreasonnames it — for examplevalue_violates(60000<=50000),mismatch_region(expected=kr,got=us), orcontext_missing_valuewhen a compared key is absent. A set rule (region={kr,jp}) is satisfied by any one of its values, and with!=by a value outside the set.uses,ttlandexpon a rule are not compared withcontext. - If the scope is registered and no rule is violated,
allowistrue.
The call must be authenticated, and {slug} must be the organization the caller is acting in; otherwise the answer is 401 or 404. The check is evaluated for the organization as a whole: userId identifies who the action is for in the audit record and does not change the result. ruleExpression is returned to machine callers and to members who hold org.permission.read; for other callers it is null. Every check, allowed or denied, is recorded as an event whose type is the scope itself (shipment.approve), with the context and the outcome.
Managed vs custom — at a glance
Section titled “Managed vs custom — at a glance”| Managed | Custom | |
|---|---|---|
| Defined by | Axowl | the org, by adding it to a role |
| Namespace | org · view | the org’s own |
| Operation runs in | Axowl | the org’s own system |
| Enforced by | Axowl’s API gates | the org’s system, by calling the check |
| Grammar · constraints · approval ceiling | — | identical |
Related: Action Library · Event & system control · Capability grants · SDK overview