User self-service workflows¶
This doc explains the refined user-variable model introduced by
rfc-user-variable-model.md: the
user_self_service category, the canonical selfService flag, the implicit
targetUser and submitter variables, and admin-only submit on behalf of
submissions.
Why a dedicated category?¶
Before this change, a "self-service" workflow — one where an authenticated portal user submits a request that targets themself — required the author to:
- Pick the
usercategory. - Add a
user-typed variable, e.g.requestor. - Toggle the Requestor preset (which secretly set
type: "user"andselfPopulate: true). - Remember to set that variable as the subject variable.
Every step was easy to skip, and any mistake silently changed who the portal
form bound the variable to. The user_self_service category collapses those
four steps into one: pick the category and the designer / engine do the rest.
What the category does¶
Picking User Self-Service in the workflow designer:
- Auto-seeds a locked
targetUservariable (type: "user",selfService: true,required: true) into the variable list. You can't rename or re-type it — renaming would silently drift from the engine's implicit synthesis inrun-creator.ts. - Wires
targetUseras the workflow's subject variable so runs are indexed by the target user id for efficient per-user lookups. - Hides the variable from the portal catalog form (self-service variables are always bound server-side).
On run creation the engine:
- Synthesises the
targetUservariable if the author left it implicit (e.g. created the workflow via the MCP API without touching the designer). - Resolves
targetUserto a full user snapshot (id, email, displayName, manager snapshot, etc.) — identical shape to any otheruservariable. - Binds
targetUserto the authenticated submitter by default, or to theonBehalfOfUserIdtarget under a permitted on-behalf-of submission (see below).
The {{submitter.*}} namespace¶
Every run — regardless of category — gets an implicit submitter object
injected for use in {{submitter.*}} interpolation and transform scripts:
floh.variables.submitter.id; // always the real authenticated caller
floh.variables.submitter.email;
floh.variables.submitter.displayName;
floh.variables.submitter.firstName;
floh.variables.submitter.lastName;
Security invariant¶
{{submitter.*}} always reflects the real authenticated caller, even
under an on-behalf-of submission. targetUser may be rebound to the
on-behalf-of target, but submitter cannot. This is deliberate: audit logs
and notification bodies that use {{submitter.*}} must never be spoofable via
the on-behalf-of override.
submitter is a reserved variable name. Authors may not declare a variable
named submitter — the workflow validator rejects any such definition with
"Variable name \"submitter\" is reserved by the workflow engine".
selfService vs the deprecated selfPopulate alias¶
The canonical flag is selfService. The older selfPopulate flag is a
deprecated alias accepted on read for one major version so existing workflow
definitions keep working without a migration. The conventions:
- Read path:
isSelfServiceVariable(v)returnstrueif either flag is set. Admin designer, portal form, form-schema-adapters, and the run creator all delegate to this helper. - Write path: the designer emits
selfServiceonly. New authoring surfaces (MCP schemas, REST schemas, config-transfer) document both flags withselfServicepreferred. - Validation: if a workflow definition carries both flags and they disagree,
the server rejects the definition with a
"conflicting ... selfPopulate is a deprecated alias"error. - Removal:
selfPopulatewill be removed in the next major after this one ships. A follow-up migration will rewrite any remaining stored definitions to emitselfServiceexclusively.
Submit on behalf of¶
Admins who need to submit a catalog request on behalf of another user
(e.g. a support engineer filing a request for someone else) can do so when
they hold the workflow:submit_on_behalf_of permission. The portal request
form exposes an extra Submit as input for those callers; plain requestors
never see it.
The wire protocol: POST /api/request-catalog/:id/submit (and
POST /api/workflows/:id/start) accept an optional onBehalfOfUserId in the
request body. The server:
- Rejects the request with
403 ON_BEHALF_OF_FORBIDDENif the caller lacksworkflow:submit_on_behalf_of. - Rejects with
400ifonBehalfOfUserIdpoints to an unknown user. - Silently drops the override if the caller targets themself — a permission-holder submitting on behalf of themself is just a normal submission, and audit entries shouldn't imply divergence.
- On success, creates the run with
initiated_byset to the real caller and thetargetUser/ subject bound to the on-behalf-of target. - Emits a
workflow.on_behalf_of_submissionaudit entry recording theinitiatorId(caller),targetUserId(on-behalf-of), and workflow id.
In transforms, notifications, and templates:
{{targetUser.*}}→ on-behalf-of target{{submitter.*}}→ real caller (the admin)run.initiated_by→ real caller
Putting it together¶
A typical user_self_service workflow definition (rendered JSON):
{
"category": "user_self_service",
"subjectVariable": "targetUser",
"variables": [
{
"name": "targetUser",
"type": "user",
"required": true,
"selfService": true,
"description": "The requesting user (auto-bound at run start)"
},
{
"name": "firstName",
"type": "string",
"required": true,
"defaultValue": "{{submitter.firstName}}"
}
],
"steps": [
/* … */
]
}
Under an on-behalf-of submission, the engine's variable snapshot at run start looks like:
{
"targetUser": { "id": "<target>", "email": "bob@example.com", "...": "..." },
"firstName": "Bob",
"submitter": { "id": "<admin>", "email": "alice@example.com", "...": "..." }
}
run.initiated_by === <admin>, run.subject_type === "user",
run.subject_id === <target>, and the audit log carries one
workflow.on_behalf_of_submission entry linking the two.
References¶
- RFC: user variable model — design doc
- Google Workspace account request walkthrough — end-to-end example
packages/server/src/modules/workflows/run-creator.ts— engine implementationpackages/server/src/modules/workflows/validation.ts— schema invariants.cursor/rules/domain/floh-workflows.mdc— authoring rules.cursor/rules/core/security-invariants.mdc— on-behalf-of audit invariants