Skip to content

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:

  1. Pick the user category.
  2. Add a user-typed variable, e.g. requestor.
  3. Toggle the Requestor preset (which secretly set type: "user" and selfPopulate: true).
  4. 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 targetUser variable (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 in run-creator.ts.
  • Wires targetUser as 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 targetUser variable if the author left it implicit (e.g. created the workflow via the MCP API without touching the designer).
  • Resolves targetUser to a full user snapshot (id, email, displayName, manager snapshot, etc.) — identical shape to any other user variable.
  • Binds targetUser to the authenticated submitter by default, or to the onBehalfOfUserId target 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) returns true if 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 selfService only. New authoring surfaces (MCP schemas, REST schemas, config-transfer) document both flags with selfService preferred.
  • 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: selfPopulate will be removed in the next major after this one ships. A follow-up migration will rewrite any remaining stored definitions to emit selfService exclusively.

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:

  1. Rejects the request with 403 ON_BEHALF_OF_FORBIDDEN if the caller lacks workflow:submit_on_behalf_of.
  2. Rejects with 400 if onBehalfOfUserId points to an unknown user.
  3. 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.
  4. On success, creates the run with initiated_by set to the real caller and the targetUser / subject bound to the on-behalf-of target.
  5. Emits a workflow.on_behalf_of_submission audit entry recording the initiatorId (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 implementation
  • packages/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