Skip to content

Roles, Permissions, and Routes ​

This documentation serves as a technical reference (including for AI) regarding the access levels in the Flowi Agentic platform.

The platform has a Multi-tenant SaaS architecture. Access control rests primarily on the user's global_role column — which is either SUPER_ADMIN or nothing — and on the user's role within the context of a Tenant, stored in user_tenant_memberships.platform_role.

No role comes from an external provider

The session is always the platform's own JWT, issued and validated by the application itself (security/JwtTokenProvider.java, service/AuthService.java). OIDC login does exist — the SSO connections under /admin/sso, handled by SsoConnectionService and SsoCallbackService — but it only authenticates: JIT provisioning writes globalRole = null (SsoCallbackService.provision), no directory group becomes a role, and no tenant membership is created. Anyone looking for an identity console to grant roles will not find one: roles are granted in the platform's own database, through /admin/users and /admin/tenants/{id}/members.


1. Profiles and Access Levels ​

A. Super Admin (Global Admin) ​

  • Frontend Identification: roles.includes('SUPER_ADMIN')
  • Backend Identification: hasRole('SUPER_ADMIN'), resolved from the global role claim in the JWT the platform issues itself.
  • Scope: The entire system. Not tied to a specific tenant in the global administration interface.
  • Responsibilities: Register tenants, manage global process definitions (BPMN/CMMN), DMN templates, configure AI, and manage all users.

B. Tenant Admin (Tenant Administrator) ​

  • Frontend Identification: !isSuperAdmin && currentTenant?.role === 'ADMIN'
  • Backend Identification: @tenantSec.isCurrentTenantAdmin(authentication) on /a/* resources. Reaches nothing under /api/admin/** — SecurityConfig requires SUPER_ADMIN on that matcher, and a Tenant Admin's authority is ROLE_ADMIN.
  • Scope: Restricted to their own Tenant's data.
  • Responsibilities: Configure the default Business Key for the tenant, enable/disable published processes in their catalog, manage the tenant's robots and runners, API Keys, Webhooks, mail inboxes, Variables and secrets and Data retention. AI agents are not robots: they live inside each process, and the Modeler configures them too. Tenant members are not on that list: /api/admin/tenants/{id}/members* is SUPER_ADMIN only.

B.1. Tenant Modeler ​

  • Frontend Identification: currentTenant?.role === 'MODELER'
  • Backend Identification: @tenantSec.isCurrentTenantModeler(authentication) — accepts MODELER and ADMIN with a membership (UserTenantMembership) in the current tenant; SUPER_ADMIN always passes. An API key has no membership and therefore never passes.
  • Scope: Restricted to their own Tenant's data.
  • The rule, stated once: MODELER is ADMIN minus the tenant's keys, its robots, its webhooks, its mail inboxes, the company variables and secrets, data retention, the tenant's AI usage and its people. They design, configure and publish processes; they do not administer the organisation.
  • Responsibilities: process catalogue, modeler and versions — including publishing a version; forms; decision tables (DMN); process roles, process variables and document types; knowledge bases and the process's AI agents; the Java delegate selector; the process documentation; the mail trigger rule (/a/mail-rules), which now lives on the definition's Configurações tab, and the delivery log (/a/mail-deliveries) — a message that started no process is a diagnosis about the design. The log names each message's inbox through the credential-free list at /a/mail-rules/inboxes, so the modeller sees the inbox name without access to the inbox registry. They also drive the modeler copilot and AI validation.
  • What they cannot reach: tenant members (/a/members), robots and runners (/a/robots, /a/robot-runners), AI robot generation (/a/robot-generations), API keys (/a/api-keys), webhooks (/a/webhooks) and mail inboxes (/a/mail-inboxes, which hold server credentials — the same nature as an API key). To pick the inbox a rule uses they read /a/mail-rules/inboxes, a projection returning only id, name and status — never host, username or password reference. Nor do they reach the company's Variables and secrets (/admin/variables, /a/tenant-variables), Data retention (/admin/retention, /a/retention) or the tenant-wide AI usage (/ai-usage, GET /a/ai-usage) — the usage of a process they can change they read at GET /a/ai-usage/process/{key}.
  • They are not a rank above the Manager. MODELER is not in PlatformRole.seesEverythingInTenant(): at runtime they see what a USER sees. Cancelling, suspending or reactivating an instance, reading the failed-job queue and following /monitor remain the Manager's.

A non-administrator now spends the tenant's money

The copilot and AI validation consume the tenant's token quota. Granting the modeler those screens is a deliberate trade — the alternative is a role that cannot use the tool the product is built around. That is why the ai_usage_logs row for a copilot turn carries the user_id on top of the tenant, not only the tenant: the governance screen has to answer who spent it. A per-person ceiling exists in user_ai_quotas and is honoured both on the preflight check and on the debit (TenantAiQuotaService); today it is only written by PUT /api/admin/users/{id}/ai-quota, which is Super Admin.

B.2. Tenant Manager ​

  • Frontend Identification: currentTenant?.role === 'MANAGER'

  • Backend Identification: @tenantSec.isCurrentTenantManager(authentication) — accepts MANAGER and ADMIN with a membership (UserTenantMembership) in the current tenant; SUPER_ADMIN always passes.

  • Scope: Restricted to their own Tenant's data.

  • Responsibilities: the manager runs the tenant at execution time and looks after content. These recognise the manager today:

    • CMS — creates, edits and deletes collections and records, bulk-loads reference tables and clears collections.
    • Instances — cancelling (DELETE /a/instances/{id}), suspending and reactivating (PATCH /a/instances/{id}/suspend and /activate), the failed-job queue (GET /a/instances/failed-jobs, retry and deadletter) and, in a CMMN case, completing a stage (POST /a/instances/{id}/plan-items/{planItemId}/complete-stage).
    • AI step failures — retry and dismiss on /a/ai-task-failures/{id}; the listing shows them every failure in the tenant.
    • Process definitions — per-step monitoring (/a/definitions/{key}/monitor and /monitor/{activityId}), the SLA metrics (GET /a/definitions/{key}/sla-metrics) and writing the process documentation (PUT and DELETE on /a/definitions/{key}/documentation). Reading the documentation only requires being authenticated.
    • Knowledge bases — every route under /a/knowledge-bases/** and the per-process base under /a/definitions/{key}/knowledge/**, reading included.
    • Reading document types — GET /a/document-types, which fills the type picker in the form builder. Creating, editing and deleting stay with the Admin and the Modeler.
    • Reading members — GET /a/members.

    The process AI-agent API (/a/definitions/{processKey}/ai-agents/**, playground included) and the model catalogues (/a/ai-models/active, /a/ai-model-classes) also accept the manager on the server, but the screen sits under Process design, which they do not open. In every other module a manager behaves as a Tenant User. When adding a new module, decide explicitly whether it should recognise the manager.

C. Tenant User (Standard User) ​

  • Frontend Identification: currentTenant?.role === 'USER'
  • Backend Identification: Restricted access via operational endpoints (/a/*), with the tenant coming from the session token.
  • Scope: Restricted to the operation of their own Tenant.
  • Responsibilities: Start process instances, execute tasks (/tasks), fill out forms, and view operational dashboards.

2. Frontend Routes (React / TanStack Router) ​

The table below crosses frontend routes with each profile's visibility — what the sidebar (components/Sidebar.tsx) shows and what each route's beforeLoad refuses. It is not the authorization matrix — section 3 is what governs — but since the modeller role it is complete: every restricted route goes through requireArea(), and the route-to-area map lives in src/lib/access.ts. There are six areas: design (Admin and Modeller), content (Admin and Manager), knowledge (Admin, Modeller and Manager), organisation (Admin only), tenantWideMonitoring (Admin and Manager) and platform (no tenant role — Super Admin only). Under /admin the default area is platform: the tenant screens that live there (/admin/webhooks, /admin/knowledge-connections, /admin/api-keys, /admin/variables, /admin/retention, /admin/process-roles, /admin/document-types, /admin/knowledge-bases) are declared one by one, and any other refuses a Tenant Admin even when typed into the address bar.

The refusal is no longer silent: requireArea() sends the person to /dashboard with ?denied=<area>, and the Dashboard explains in translated text why that screen is not theirs.

A ❌ in the Super Admin column means "not their route", not "blocked": no beforeLoad refuses a Super Admin who has selected a tenant — he then sees and opens the same tenant entries as the Admin. Without a tenant selected, he opens only the platform screens (the platform area: everything under /admin that is not a tenant's), the Dashboard, the profile and the company choice; any other screen — including the tenant ones that live under /admin, such as Webhooks, Variables, Retention, API Keys, Document Types, Process Roles and Knowledge Bases — sends him to /select-tenant. To return to that state after choosing a company, he uses Whole platform in the header's company selector, which exchanges the token for a tenant-less one at POST /api/auth/select-platform.

Route / PathModule / FunctionSuper AdminTenant AdminModellerManagerTenant User
/dashboardOverview, charts, and counters.❌ (Uses admin panel)✅✅✅✅
/admin/tenantsGlobal listing and creation of tenants.✅❌❌❌❌
/admin/usersGlobal listing of users.✅❌❌❌❌
/admin/ai-configAI configuration, in two tabs: LLM Model Catalog and Vector Database (RAG).✅❌❌❌❌
/definitionsLocal Definitions (Process Catalog)
- For the Admin and the Modeller: views published processes, enables/disables, publishes a version, and configures local instances, document types and the e-mail trigger.
❌✅✅❌❌
/admin/global-templatesGlobal Templates
- For SA: Lists all, allows creating BPMN/CMMN/DMN and configuring global Document Types, Variables, and Attachments.
✅❌❌❌❌
/admin/process-rolesCentralized catalog for Process Roles.✅❌✅❌❌
/admin/global-templates/$keyTemplate details and advanced configuration tabs.✅❌❌❌❌
/instancesListing of active instances, history, and errors.❌ (Global management via API only)✅✅✅✅
/instances/start/*Form to start instances.❌ (Restricted to prevent tenant-less instances)✅✅✅✅
/tasksInbox and human task execution.❌✅✅✅✅
/cmsDynamic content management for the tenant.
- The screen requires a Manager or an Admin, and sits under Work; a Modeller and a standard user are refused, and told why on screen, because every CMS write is Manager or above.
- Writing (create/edit/delete/bulk load): Manager or above.
- ⚠️ The read API (GET /a/cms/**) is still open to any authenticated member, bounded by the collection's confidentiality level. Hiding the route in the frontend does not close the endpoint.
❌✅❌✅❌
/admin/knowledge-basesThe tenant's knowledge bases, under Process design (for a Manager, under Work).
- The screen requires a Manager, a Modeller or an Admin, and the API requires the same role — reading included. Hiding the route on the front end is not what closes the endpoint.
❌✅✅✅❌
/monitorPer-step monitoring: process diagram with the work parked at each activity. Manager or above.❌✅❌✅❌
/admin/document-typesThe tenant's document type catalogue, under Process design.
- The screen requires an Admin or a Modeller, and the API requires the same to create, edit and delete.
- Reading (GET /a/document-types) goes down to Manager, because it is what fills the type selector in the form builder; an ordinary user gets 403.
❌✅✅❌❌
/decisionsDecision Tables (DMN), under Process design → Decision Tables.
- beforeLoad (requireArea('design')) refuses anyone who does not design processes — Admin, Modeller and Super Admin pass — because GET /a/decisions already required that level: a plain user only saw an empty list and buttons that answered 403.
- ⚠️ Evaluation (POST /a/decisions/{key}/evaluate) remains open to any authenticated member — that is how processes consult the table.
✅✅✅❌❌
/robotsAutomation and bots linked to the tenant.
- The screen requires a tenant Admin, and the API requires the same on every /a/robots/** route — including reading and writing the parameter configuration, which is where a secret reference comes in.
❌✅❌❌❌
/admin/api-keysProgrammatic API keys per tenant.❌✅❌❌❌
/admin/webhooksConfigured callbacks per tenant.❌✅❌❌❌
/admin/knowledge-connectionsKnowledge connections to Microsoft 365 (SharePoint and OneDrive), under Integrations and automation. They decide how the platform authenticates against the customer's directory, so they belong to the Tenant Admin; base curators pick the connection and the folder in the Sources tab.❌✅❌❌❌
/admin/variablesCompany Variables and secrets, under Administration — variables and secrets that apply to the whole tenant, outside any process.
- The screen requires a tenant Admin (organisation area), and the /a/tenant-variables/** API requires the same.
❌✅❌❌❌
/admin/retentionData retention, under Administration — the retention window of each tenant table.
- The screen requires a tenant Admin (organisation area), and the /a/retention/** API requires the same.
❌✅❌❌❌
/ai-usageThe tenant's AI Usage, under Administration.
- The screen requires a tenant Admin (organisation area).
❌✅❌❌❌
/robots/runnersRunners — the machines that run robots, under Integrations & automation.
- It falls under the /robots prefix (organisation area), and the /a/robot-runners/** API requires a tenant Admin.
❌✅❌❌❌
/admin/tenants/$idTenant member management. Super Admin only: beforeLoad redirects a Tenant Admin to the Dashboard, and every /api/admin/tenants/** endpoint requires SUPER_ADMIN.✅❌❌❌❌
/mail-inboxesThe tenant's mail inboxes, under Integrations & automation, next to API Keys and Webhooks.
- It holds the IMAP connection and the password reference: it is a credential, which is why it stays with the tenant Admin.
- The rule list is still there, read-only, so an administrator can see what depends on that account before changing or deleting it.
❌✅❌❌❌
/mail-deliveriesEvery message the platform read, and what it did with it.
- A refused message is a diagnosis about the process that should have started, so the modeller sees it too.
❌✅✅❌❌

/admin/rag-config is not a frontend route

rag-config exists only as a backend endpoint (/api/admin/rag-config). Navigating to /admin/rag-config in the SPA opens nothing — RAG configuration is the second tab of /admin/ai-config. That applies to any link or document pointing there.


3. Backend Endpoints and Permissions (REST API) ​

The APIs in Spring Boot are divided by URL convention and security filters.

The catalogue of paths, methods and parameters is REST API; what this page adds is who may call each one and what the backend does when the caller may not.

Main Prefixes ​

  1. /admin/**: Global management endpoints.
    • The security filter requires SUPER_ADMIN. ADMIN is a role held inside a tenant and opens nothing under /api/admin/**; each controller narrows from there, and most operations are SUPER_ADMIN only (see the administration table in REST API).
    • Operate with no selected tenant, cross-tenant where applicable.
  2. /a/**: Multi-tenant operational endpoints.
    • Require a tenant selected in the token (POST /api/auth/select-tenant).
    • The backend validates if the authenticated user has the listed tenant in their permissions.
    • The prefix only guarantees an authenticated user in the tenant. The role is decided by each controller's @PreAuthorize, and there are four tenant predicates: isCurrentTenantAdmin (ADMIN only), isCurrentTenantManager (MANAGER or ADMIN), isCurrentTenantModeler (MODELER or ADMIN), and the deliberate absence of an annotation, listed under Routes left open on purpose. SUPER_ADMIN passes on all of them. A new route with no role decision fails TenantEndpointDeclaresItsRoleTest.

Key Process Control Endpoints ​

There is no /admin/definitions

Deploying, versioning and publishing BPMN/CMMN live at /a/definitions/..., tenant-scoped through the token. An integration written against /admin/definitions gets a 404. The fact that definitions are global does not change the path: what is global is the published artefact, not the route.

EndpointMethodContextAccessDescription
/a/definitionsGETTenantAny authenticated userReturns the active process catalog for the tenant. Authentication is enough.
/a/definitions/deployPOSTTenantTA, ModelerDeploys the BPMN/CMMN file (multipart). Does not publish the version.
/a/definitions/{key}/versionsGETTenantTA, ModelerVersion history; the published one carries published: true.
/a/definitions/{key}/versions/{definitionId}/publishPOSTTenantTA, ModelerMoves the publication to the given version.
/a/definitions/{key}/configPUTTenantTA, ModelerProcess-specific configuration in the tenant (Business Key Template and attachment policy).
/a/definitions/{key}/attachment-policyGETTenantTA, ModelerThe process's effective attachment policy. A read made by the catalogue screen, which already requires the tenant Admin. What enforces the policy at runtime is the server (AttachmentAccessService), not the form: no runtime screen calls this route, which is why it is closed.
/a/definitions/{key}/mcp-policyGETTenantTA, ModelerWhat an assistant connected over MCP may do in the process. Same design as the attachment policy: what enforces it is ExecutionMcpTools, inside the server.
/a/delegatesGETTenantTA, ModelerLoaded delegate beans, carrying each one's Java class name and declared parameters — an information surface about the installation. Only the modeler's selector consumes it, and the modeler already requires the tenant Admin.
/a/delegates/activeGETTenantTAActive global delegate JARs. Already required a role before #115.
/a/definitions/{key}/monitorGETTenantTA, ManagerWork parked per step of the process, broken down by reason. Accepts ?version=.
/a/definitions/{key}/monitor/{activityId}GETTenantTA, ManagerWhat is parked at that step: instance, reason, assignee or error. Accepts ?version=.
/a/definitions/{key}/togglePATCHTenantTA, ModelerEnables/Disables the process in the tenant's catalogue.
/a/definitions/{key}/suspendPUTTenantTA, ModelerSuspends the definition: no new instance starts. Distinct from disabling it in the catalogue.
/a/definitions/{key}/activatePUTTenantTA, ModelerReactivates the suspended definition.
/a/instancesPOSTTenantTA, UserStarts a workflow instance on behalf of the logged-in tenant. Generates the Business Key according to fallback hierarchy.
/a/instances/searchGETTenantTA, UserSearches for instances based on variables tied to the logged-in tenant.
/a/instances/{id}DELETETenantTA, ManagerCancels an instance. Destructive — not offered to an ordinary user, and refused by the backend.
/a/instances/{id}/suspendPATCHTenantTA, ManagerSuspends the instance.
/a/instances/{id}/activatePATCHTenantTA, ManagerReactivates a suspended instance.
/a/instances/failed-jobsGETTenantTA, ManagerThe tenant's failed job queue.
/a/instances/failed-jobs/{jobId}/retryPOSTTenantTA, ManagerRe-runs the job.
/a/instances/failed-jobs/{jobId}/deadletterPOSTTenantTA, ManagerMoves the job to the deadletter.
/a/instances/{id}GETTenantTA, UserInstance detail. An ordinary user only reaches an instance they take part in; another's answers 404, not 403 — a 403 would confirm it exists.
/a/instancesGETTenantTA, UserPaginated list of active instances. Accepts page, size (default 20, capped at 100) and type. Returns the {content, number, size, totalElements, totalPages} envelope — not an array. The page index is called number in the response and page in the request, following the Spring Data shape used by the other paginated routes.

Who can see an instance ​

A tenant manager or administrator sees every instance. An ordinary user sees the instances they take part in, from three sources combined:

  • the ones they started;
  • the ones they take part in — a task assigned to them, or one they are a candidate for, directly or through one of their process roles;
  • the ones they took part in — a task they already completed.

Deliberately the same rule the Tasks screen applies: two screens disagreeing about who is involved is worse than either rule alone.

"Sees everything in the tenant" has one definition, in PlatformRole.seesEverythingInTenant(), and it covers SUPER_ADMIN, ADMIN and MANAGER. MODELER and USER are left out — and the modeler was left out by decision, not by the ordering of the enum: they design the process, but at runtime they see what an ordinary user sees. Both screens read it through the same entry point (UnifiedTaskService.visibilityOf); neither carries a role list of its own. The caller's role is read first from the JWT authorities — the only source that carries the platform-wide SUPER_ADMIN, who has no membership in the tenant — and then from the membership's platformRole.

The filter covers the list, the detail, the diagram and the variable search — a search that saw past the list would make the filter a formality.

Attachment Endpoints ​

Process instance attachments (entityType = PROCESS_INSTANCE) are not governed by the platform role but by the user's role in that definition, following the policy configured at /a/definitions/{key}/config. Administrators pass every check. Other attachment types — knowledge base, process knowledge, global attachments — follow their own modules' rules and are unaffected.

EndpointMethodAccessDescription
/a/attachmentsPOSTPer uploadMode + uploadRolesUpload. Refused in FORM_ONLY for PROCESS_INSTANCE.
/a/attachmentsGETTenant memberLists the entity's current attachments (superseded ones are left out).
/a/attachments/capabilitiesGETTenant memberWhat the current user can do: canUpload, canPreview, canReplace. This is what the UI asks instead of duplicating the rule.
/a/attachments/{id}GETpreviewRolesAttachment metadata.
/a/attachments/{id}/downloadGETpreviewRolesFile contents.
/a/attachments/{id}/replacePOSTreplaceRolesUploads the new version and marks the previous one superseded.
/a/attachments/{id}/historyGETpreviewRolesThe entity's superseded versions.
/a/attachments/{id}DELETENobody, for PROCESS_INSTANCERefused even for administrators. Still valid for the other types.

An empty role list does not mean "open"

An empty list (previewRoles, replaceRoles, uploadRoles) means any user holding some role in that definition. Someone with no role in the process does not reach its attachments. A process with no configured policy behaves as AD_HOC under that same default rule.

A task inside the process acts as the system ​

The policy above answers "which person may open this file". A task running inside the process has no person: in an asynchronous service task the job executor thread carries no SecurityContext at all. That is why a process task has system access to everything inside its own tenant — the person who designs the process is the administrator, and each step inherits that authority instead of borrowing the identity of whoever started the instance.

System access requires both conditions at once: being inside an agent tool call and having a routed tenant. If the agent declares a different tenant from the one routed on the connection, access is refused and the mismatch is logged at error level.

The boundary between tenants does not depend on that check. The attachments table lives in the tenant_<slug> schema and the connection is routed by TenantAwareDataSource — an agent cannot reach another tenant's attachment even if every permission check were removed, because the connection cannot see the other schema.

Deletion stays refused for everyone, the system included: on PROCESS_INSTANCE the only write path is replacement.

CMS Endpoints ​

Reading is open to any tenant member; writing requires Manager or above. In both cases access still depends on the collection's confidentiality level (see section 5).

EndpointMethodAccessDescription
/a/cms/collectionsGETSA, TA, Manager, UserLists only the collections the caller has clearance to read.
/a/cms/collectionsPOSTSA, TA, ManagerCreates a collection with its fields and confidentiality level.
/a/cms/collections/{id}GETSA, TA, Manager, UserCollection detail.
/a/cms/collections/{id}PUT, DELETESA, TA, ManagerChanges the schema/confidentiality or deletes the collection.
/a/cms/collections/{id}/recordsGETSA, TA, Manager, UserLists records, paginated.
/a/cms/collections/{id}/recordsPOSTSA, TA, ManagerCreates a record.
/a/cms/collections/{id}/recordsDELETESA, TA, ManagerDeletes all records of the collection (clean reload).
/a/cms/collections/{id}/records/bulkPOSTSA, TA, ManagerIdempotent bulk load of reference tables.
/a/cms/collections/{id}/records/{rid}PUT, DELETESA, TA, ManagerUpdates or deletes a record.

Calls authenticated by API Key (X-API-Key) write to the CMS — that is how a reference table gets loaded by a script — but with clearance fixed at Public: a leaked key does not reach classified data.

The key still needs the scope for the method: read for the GETs in the table above, write for the POST, PUT and DELETE. Without it the request stops in the filter with a 403, before any role is evaluated. Detail in REST API → Scopes.

Knowledge Base Endpoints ​

Unlike the CMS, reading also requires Manager, Modeler or above: the listing of bases and documents shows what the AI consults, and that is curation, not content for consumption. An ordinary user gets a 403 on every line below.

EndpointMethodAccessDescription
/a/knowledge-basesGETSA, TA, Manager, ModelerLists the tenant's bases and the global ones visible to it.
/a/knowledge-basesPOSTSA, TA, Manager, ModelerCreates a base. Flagging isGlobal requires Super Admin — the service refuses anyone else.
/a/knowledge-bases/{id}GETSA, TA, Manager, ModelerBase details with the document count.
/a/knowledge-bases/{id}DELETESA, TA, Manager, ModelerDeletes the base. Refused while a process is still linked. A global base only by the Super Admin.
/a/knowledge-bases/{id}/search-settingsPATCHSA, TA, Manager, ModelerChanges the keyword search language. A global base only by the Super Admin.
/a/knowledge-bases/{id}/documentsGETSA, TA, Manager, ModelerLists the documents, paginated.
/a/knowledge-bases/{id}/documentsPOSTSA, TA, Manager, ModelerUploads PDF, TXT or MD; indexing runs in the background.
/a/knowledge-bases/{id}/documents/{docId}DELETESA, TA, Manager, ModelerRemoves the document and its vectors.
/a/knowledge-bases/{id}/documents/{docId}/classificationPUTSA, TA, Manager, ModelerChanges the document level (PUBLIC, SECRET, TOP_SECRET). Beyond the role, the caller must reach the current and the new level (Modeler → Public, Manager → Secret, Admin → Top secret); 403 otherwise. Re-upload, restore, re-ingest, download and delete follow the same rule.
/a/knowledge-bases/{id}/default-classificationPUTSA, TA, Manager, ModelerLevel of the next documents; applyToExisting reclassifies the current ones and requires reaching all of them.
/a/knowledge-bases/{id}DELETE (level)SA, TA, Manager, ModelerBeyond the role, deleting the base requires reaching its most restricted document. KB_DOCUMENT files do not leave through /a/attachments/** (404); only through the base routes.
/a/knowledge-bases/{id}/access-logGETSA, TASearches that returned or denied a classified excerpt of the base, paginated. Admin only: it lists who read restricted material.
/a/knowledge-bases (kind: EXTERNAL)POSTSA, TACreates an external base (Elasticsearch / OpenSearch). Managers and Modelers get 403: the base sets a network destination and a credential. Global only by the Super Admin, with a vault reference.
/a/knowledge-bases/{id}/externalPUTSA, TAChanges the external base connection. The credential is the name of a secret variable or a vault reference, never the value.
/a/knowledge-bases/{id}/external/checkPOSTSA, TATest connection: reads the index mapping, the fields and the vector size.
/a/knowledge-bases/{id}/testPOSTSA, TA, Manager, ModelerTest a query on an existing external base, at the caller's access level.
/a/knowledge-bases/egressGET, PUTSA, TAAllowed destinations of the tenant's external bases and the status of each base.
/a/knowledge-bases/{id}/documents/**all—409 on an external base: it is queried at the source and takes no documents.
/a/knowledge-bases/process/{key}GETSA, TA, Manager, ModelerBases linked to that process.
/a/knowledge-bases/process/{key}/link/{id}POSTSA, TA, Manager, ModelerLinks the base to the process.
/a/knowledge-bases/process/{key}/unlink/{id}DELETESA, TA, Manager, ModelerUndoes the link.
/a/knowledge-sources/providersGETSA, TA, Manager, ModelerSync providers enabled on the installation and the fixed hosts of each cloud.
/a/knowledge-bases/{id}/sourcesGET, POSTSA, TA, Manager, ModelerThe base's synced sources. Creating one requires reaching the chosen level; the list hides sources above the caller's level.
/a/knowledge-bases/{id}/sources/{sid}PUT, DELETESA, TA, Manager, ModelerEdits or removes the source (?keepDocuments=); requires reaching its level.
/a/knowledge-bases/{id}/sources/{sid}/estimate · /sync · /confirm-deletes · /cancel-deletes · /failures/retryPOSTSA, TA, Manager, ModelerFirst-sync estimate (files, bytes, token ceiling — no money), sync now, confirm or cancel a mass deletion, reopen failures whose retries ran out.
/a/knowledge-bases/{id}/sources/{sid}/runs · /failuresGETSA, TA, Manager, ModelerLatest rounds and failed files, with codes translated on screen.
/a/knowledge-connectionsGET, POSTSA, TAMicrosoft 365 connections. The body refuses any field besides name, directory, client ID, cloud and the name of the secret variable.
/a/knowledge-connections/{id}PUT, DELETESA, TAEdits or deletes; deleting is refused (409) while a source uses the connection.
/a/knowledge-connections/{id}/testPOSTSA, TATests the login host, the token and the Graph host, one by one.
/a/knowledge-connections/{id}/browse · /optionsGETSA, TA, Manager, ModelerBrowse sites, libraries and folders through the connection, and list connections (no credential) for the source wizard.

The per-process knowledge base — the one the catalogue's RAG tab feeds — is the same capability reached by another route, so it requires the same role. Closing /a/knowledge-bases without closing these would leave an ordinary user wiping here the collection they cannot touch there.

EndpointMethodAccessDescription
/a/definitions/{key}/knowledge/documentsGETSA, TA, Manager, ModelerLists the documents indexed for that process.
/a/definitions/{key}/knowledge/documentsPOSTSA, TA, Manager, ModelerUploads a document and triggers indexing.
/a/definitions/{key}/knowledge/documents/{docId}DELETESA, TA, Manager, ModelerRemoves the document and its vectors.
/a/definitions/{key}/knowledge/countGETSA, TA, Manager, ModelerNumber of chunks indexed for the process.
/a/definitions/{key}/knowledgeDELETESA, TA, Manager, ModelerWipes all vectors of the process. Destructive.
/a/definitions/{key}/knowledge/reindexPOSTSA, TA, Manager, ModelerClears and reindexes every attached file.

An API key does not reach these routes: it carries no tenant membership, and the role check requires one.

Document Type Endpoints ​

A document type is referenced by the form's File field, but whoever fills the form never calls these routes: the documentTypeId is already stored in the published schema and goes straight to /a/attachments. That is why reading stays at Manager or Modeler — it is the form builder's selector — while maintaining the catalogue is the Modeler's, who is the one designing the form that points at the type.

EndpointMethodAccessDescription
/a/document-typesGETSA, TA, Manager, ModelerLists the tenant's types, or a process's global ones with ?isGlobal=true&processKey=. Reading the global ones stays open to the tenant: they are the types of the process it subscribes to.
/a/document-typesPOSTSA, TA, ModelerCreates a tenant type. With ?isGlobal=true, SA only.
/a/document-types/{id}PUTSA, TA, ModelerChanges name, description or status. If the type is global, SA only.
/a/document-types/{id}DELETESA, TA, ModelerDeletes the type. A published form pointing at it loses the reference. If the type is global, SA only.

The global layer is not written by a tenant Admin

isGlobal=true writes into the shared layer: the type comes to hold for every tenant that subscribes to the process. The tab that authors those types lives under Global Templates, which in the UI is Super Admin, yet the API accepted the flag from any tenant Admin — and a tenant Admin only answers for its own tenant. POST ?isGlobal=true, and the PUT/DELETE of a type that is already global, now answer 403 to anyone who is not a Super Admin. This is the same design that already protected the global knowledge base (KnowledgeBaseService.createKnowledgeBase) and the global agent (AiAgentService.resolveTenant), and all three cases live in the same test, GlobalLayerIsNotOpenTest.

Robot Endpoints ​

A robot is code the tenant publishes and the platform runs with the tenant's credentials. That is why every /a/robots/** route requires a tenant Admin — registration, JAR upload, execution, state, logs, and also the parameter configuration, which is where the secret comes in: a parameter marked secret holds a reference (${var.erpToken}) to the tenant variable that carries the token. Reading the configuration already names the declared parameters and shows where each reference points, so read and write sit at the same role — Manager and ordinary user get 403 on both. The /robots/$key screen, the only consumer of these routes, already required a tenant Admin in its beforeLoad; it was the route that stood open.

EndpointMethodContextAccessDescription
/a/robots/{key}/configGETTenantSA, TAThe robot's parameter configuration in that tenant, together with the parameters declared by the JAR.
/a/robots/{key}/configPUTTenantSA, TAWrites the parameter configuration. This is where a secret parameter's reference comes in.

Pinned by RobotConfigAuthorizationTest.

Generating a robot with AI (/a/robot-generations/**) follows the same rule: tenant Admin on every route — generate, refine, edit, approve, test, activate and discard. Generating a robot is creating one, and whoever approves the code must be able to fill in its secret parameters. The Modeler gets 403 here as on /a/robots/**, and the Super Admin only gets in with a tenant selected: without one the answer is 400, because there is no global robot generation. Pinned by ModelerDoesNotAdministerTheTenantTest and TenantEndpointDeclaresItsRoleTest.

Conducting a Case (CMMN) ​

The case plan is conducted by whoever works on the case, and closing the stage is the only management action.

EndpointMethodContextAccessDescription
/a/instances/{id}/plan-itemsGETTenantTA, Manager, UserThe case's live steps, each with the actions it accepts. COMPLETE_STAGE is offered only to whoever manages the tenant.
/a/instances/{id}/milestonesGETTenantTA, Manager, UserMilestones the case has already reached.
/a/instances/{id}/plan-items/{planItemId}/startPOSTTenantTA, Manager, UserStarts the offered step.
/a/instances/{id}/plan-items/{planItemId}/disablePOSTTenantTA, Manager, UserDiscards the optional step.
/a/instances/{id}/plan-items/{planItemId}/enablePOSTTenantTA, Manager, UserOffers a discarded step again.
/a/instances/{id}/plan-items/{planItemId}/triggerPOSTTenantTA, Manager, UserFires a waiting user event.
/a/instances/{id}/plan-items/{planItemId}/complete-stagePOSTTenantSA, TA, ManagerCloses the stage, ending the optional steps still offered inside it. An ordinary user gets 403.

The first six rows are not "open to any authenticated user": all of them go through WorkflowInstanceService.requireVisible, the same gate as the instance detail, and a case the person does not take part in answers 404. Pinned by CasePlanItemControllerAuthorizationTest and CasePlanItemServiceTest.

Modeler AI Endpoints ​

EndpointMethodContextAccessDescription
/a/ai-validation/bpmnPOSTTenantSA, TA, ModelerSemantic BPMN validation by LLM. Manager and ordinary user get 403: the screens that trigger the call (/definitions, /definitions/{key}, /admin/global-templates/{key}) belong to designing the process, and every call spends the tenant's token quota. An exhausted or never-configured quota answers 402, not a validation opinion.
/a/modeler/copilot/streamPOSTTenantSA, TA, ModelerModeler copilot over SSE. The row written to ai_usage_logs carries the user_id of whoever drove the turn, on top of the tenant and the turn's executionCorrelationId — without it governance would know the tenant spent, but not who. The body carries processKey (the key of the process open in the modeler) and history (up to 12 earlier messages, role user or assistant). The server pins that key on every tool that takes processKey and trims the history to 24,000 characters.
/a/modeler/copilot/applyPOSTTenantSA, TA, ModelerCreates what the copilot proposed in the turn (form, agent, knowledge base, collection), on behalf of whoever clicked Create all. During the turn the copilot only proposes and writes nothing outside the diagram. It accepts only those seven creation tools, runs them in order and swaps provisional references for real ids. Each tool also checks its own role. It calls no model.
/a/modeler/copilot/validatePOSTTenantSA, TA, ModelerChecks, with the engine, the diagram a copilot turn drew. Takes { "xml", "processKey" } and returns { "findings" }, each with severity (ERROR or WARNING), code, elementId, elementName and message. With processKey, it also flags a step that uses an agent or a form the process does not have. It calls no model and spends no tokens. Flowi's own rules (a step with no form, an agent step with no agent, a decision exit with no condition) are warnings only and never block a deploy.

Routes Left Open on Purpose ​

Under /a/**, a missing @PreAuthorize means "any authenticated user of the tenant". On the four rows below that is a decision, not an oversight — closing them would break a screen an ordinary user needs. Each one has its limit next to its promise, and each limit has a test that pins it down.

EndpointMethodWhy it stays openWhere the limit is
/a/attachments, /a/attachments/{id}/replace, /a/attachments/{id}POST, DELETEAn ordinary user attaches a file through their own task form — the entityType=process path, which is ungoverned. Requiring Admin on the route would take attachments out of the process.Inside the service, by process role: AttachmentAccessService.requireUpload/requireReplace/requireDeletable, following the process policy's uploadRoles/replaceRoles (see Attachment Endpoints above). Pinned by AttachmentControllerAuthorizationTest and AttachmentAccessServiceTest.
/a/notifications, /a/notifications/{id}/read, /a/notifications/read-allGET, PUTEvery user has an inbox; no role applies.The userId comes from the token, never from the body or the URL. NotificationService.markRead refuses when the notification belongs to someone else, and markAllRead is an UPDATE ... WHERE user_id = :userId. Passing someone else's notification id answers 403 and writes nothing — an access refusal, not a malformed request, so the caller can tell "no such id" (404) from "not yours" (403). Pinned by NotificationControllerAuthorizationTest and NotificationServiceTest.
/a/dashboardGETIt is the landing screen of any tenant member.The tenant and the viewer's visibility. Active tasks and finished processes go through UnifiedTaskService.visibilityOf — the same entry point Tasks and Instances read: whoever sees the whole tenant reads the total, everyone else reads only what they take part in. The definition counter stays tenant-wide, because starting a process remains open to an ordinary user. Pinned by DashboardFollowsInstanceVisibilityTest and DashboardCountsOnPostgresTest.
/a/instances/{id}/plan-items/{planItemId}/start, /disable, /enable, /triggerPOSTConducting your own step is the work of whoever works on the case, and the case screen carries no role beforeLoad. Requiring Manager would take away from an ordinary user the case they started themselves.CasePlanItemService calls WorkflowInstanceService.requireVisible before any transition: a case the person does not take part in answers 404. The engine refuses a transition the state does not allow, and availableActions hides only COMPLETE_STAGE. Pinned by CasePlanItemControllerAuthorizationTest and CasePlanItemServiceTest.

The dashboard card counts what the viewer can see

GET /a/dashboard no longer returns the same numbers to everyone. Active tasks and finished processes are counted by the same rule the listing applies: the service reads UnifiedTaskService.visibilityOf(tenant, user) — the one place where "sees everything in the tenant" is written, and the same one InstanceVisibilityService consults. Manager, Admin and Super Admin still read the tenant total; an ordinary user counts only the tasks they are assignee or candidate for (directly or through a process-role group), and only the finished processes they held a task in or started.

The scope is applied as a predicate of the count itself, never by materialising the list of visible ids: DashboardCountRepository keeps the promise of two queries per load, however many instances the tenant holds. The definitions counter stays tenant-wide on purpose — it is the catalog of what you may start, and POST /a/instances remains open to an ordinary user.


Who may act on a task ​

Claiming, completing, saving a draft, returning, delegating and commenting on a task do not depend only on being authenticated in the tenant. ADMIN, MANAGER and SUPER_ADMIN act on any task. Everyone else, MODELER included, acts only as the assignee, the owner (delegate) or a candidate (by user or by process role), and a task already assigned to someone else is completed only by them. A task that has nothing to do with the caller answers 404; one in an instance the person started or worked on, but which they cannot act on, answers 403. Delegating requires the target to be a member of the tenant. The same applies to human tasks of CMMN cases. Reading the task (GET /tasks/{id}, /variables, /comments, /field-provenance, /ai-suggest) needs the same relation or having started or worked on the instance; strangers get 404. An API key neither acts on nor reads a task: it gets 403 with an explicit message, because claiming, completing and reading a task are acts of a person; use a user token.

Reserved variables ​

Some variable names belong to the platform: aiDecision and aiDecisionJustification (written only by the agent's routing tool), escalateToHuman, escalationReason, escalationUrgency, the AI step configuration (aiAgentId, aiInstruction...) and the internal names (tenantId, and any name starting with _ or ai_). No request may send them: completing or saving a task, starting an instance (with a user token, an API key or a robot), calling the MCP execution tools, and creating a tenant variable with such a name all answer 400 with the list of names. Without this rule, anyone able to complete a task would pick the exit of an AI-decided gateway. If a form field needs one of these names, rename the field.

4. Hierarchy Resolution for Business Keys (Note for AI) ​

The backend resolves initialization variables like the Business Key in a hierarchical order (Implemented in WorkflowDefinitionService):

  1. Explicit key in the request: a businessKey sent to POST /a/instances wins over the template. The template is a default for callers that do not know the key, not an override of callers that do. A blank or absent value falls through to the template.
  2. Tenant template (PublishedDefinition.businessKeyTemplate): the Tenant Admin writes it through /a/definitions/{key}/config.
  3. System Default: hardcoded fallback to DOC-${date:yyyyMMdd}-${random:4} (WorkflowDefinitionService.DEFAULT_BUSINESS_KEY_TEMPLATE).

Those two steps are all there is. There is no global Business Key layer: GlobalProcessTemplate has no template field, and resolveBusinessKeyTemplate goes straight from a blank tenant template to the system default.

The template accepts ${name:-fallback}, which resolves to the process variable name when it is present and non-blank, and to the fallback otherwise. The fallback may itself contain a placeholder, so one template serves both an integration that knows the data and a start form that does not:

${tipoDoc:-DOC}-${emissao:-${date:yyyyMMdd}}-${cnpjEmissor:-${random:4}}

  API, variables present  ->  NFSE-20260305-22060673000170
  UI, variables missing   ->  DOC-20260807-9O63

A placeholder with no fallback and no matching variable refuses the start. The request answers 422 Unprocessable Entity, and the instance is never created — no task, no history, nothing to cancel later. The error body names every missing variable under fieldErrors, so the client knows exactly what to send on the second attempt:

json
{
  "status": 422,
  "error": "Unprocessable Entity",
  "fieldErrors": [
    { "field": "cnpjEmissor", "message": "variável exigida pelo businessKeyTemplate e não informada" }
  ]
}

The same refusal applies when substitution empties the key entirely — a template that would resolve to NFSE--, all separators and no content, is rejected rather than stored. The business key is how the document is found later; a truncated key creates a record nobody can locate, and the 422 trades that silent problem for an error at the moment it happens.

If you need the process to start without every variable, there are two ways out: give the placeholder a fallback (${cnpjEmissor:-${random:4}}), or send an explicit businessKey in the body of POST /a/instances, which wins over the template.


5. CMS Data Confidentiality ​

Every CMS collection carries a confidentiality level. The level required to read or write a collection derives from the user's role in the tenant — there is no per-user grant.

LevelMinimum roleTypical content
PUBLICTenant UserNCM, CFOP, CEST, cost centres, product catalog
SECRETTenant ManagerMargins, commercial policy, supplier price lists
TOP_SECRETTenant AdminPayroll, legal, board material

Derived rules:

  • USER → PUBLIC, MODELER → PUBLIC, MANAGER → SECRET, ADMIN → TOP_SECRET, SUPER_ADMIN → TOP_SECRET. The modeler stays at PUBLIC on purpose: the CMS is tenant content, not process design, and CmsController requires a Manager — giving them any other level would be a promise no route keeps.
  • Collection listing omits what the caller cannot read; direct access answers 403.
  • A manager cannot classify a collection as TOP_SECRET — that would let them create data beyond their own reach.
  • AI agents: the queryCollectionTool respects the level configured on each agent (cmsClearance, default PUBLIC). BPMN processes have no logged-in user to inherit permission from, so the level is an explicit setting on the agent. In the playground the lower of the user's level and the agent's applies (AiPlaygroundController calls CmsClassification.lowest). The execution copilot does not do that arithmetic: AiExecutionCopilotController uses cmsAccessService.currentClearance() alone, so there the ceiling is the user's role, not the agent's.
  • Robots (RobotCmsApi) are not bounded by confidentiality: they are code published by tenant administrators and reach any collection in it.

Implementation reference: security/CmsAccessService.java, security/TenantSecurityService.java and the spec specs/content/cms.md.

Flowi Agentic — Plataforma de Gestão de Processos com IA