External Vendor Auto-Provision Workflow¶
A step-by-step guide to building a workflow that auto-provisions (or
reuses) a Floh user for an external vendor contact and grants a
time-bound role with no approval gate. Access is bounded by the
workflow:provision_users permission check inside user_create and by
durable role expiry (role_grant.expiresAt + the role-expiry-check
scheduler), not by a manual sign-off.
This matches the External Vendor Auto-Provision starter in the New Workflow gallery. You can instantiate that starter and skip to Step 6 — Publish, or rebuild it by hand below.
Prerequisites¶
- A vendor role the contact should receive for the access window, created under People → Roles.
- The
workflow:create,workflow:update,workflow:publish, andworkflow:readpermissions on your admin account (the Workflows tab andGET /api/workflowsare gated onworkflow:read), andtemplate:readif you instantiate the starter from the gallery. The account that runs this workflow also needsworkflow:startandworkflow:provision_users—user_createrefuses to insert an unconfirmed user without the latter, and every outcome is captured in aworkflow.user_auto_provisionedaudit row.
Security model — read this before publishing¶
- Do not wait out the access window with the
delayconnector.delayis an in-processsetTimeout. A day-scale wait keeps the runrunninguntil stuck-run recovery (default 30 minutes) fails it — leaving the grant in place with no revoke. Userole_grant.expiresAtand let therole-expiry-checkscheduler expire the assignment. See the Time-Bound Group Access starter for the same pattern. - Fail closed on duplicate grants.
role_revokeand the expiry job key assignments by user+role, not by "the assignment this run created". DefaultonDuplicate: "skip"would let a second run succeed and a later expiry would tear down a pre-existing assignment this workflow never owned. SetonDuplicate: "error". - Fail closed on partial entitlement provision.
findExpired()only selectsactiveassignments. DefaultfailOnPartial: falseleaves apartially_provisionedrow whose surviving entitlements never expire. SetfailOnPartial: true. - Validate duration, email, and display name before provisioning. A
zero/negative
expiresInDays, a boolean/array/objectexpiresInDays(Number(true) === 1andNumber([365]) === 365would otherwise provision), a non-stringvendorEmail, or a provided non-stringvendorDisplayName(start payloads type variables asunknown; an array or object JSON-stringifies into a malformed account /display_name) must not create a Floh user. Put that check in a transform that runs beforeuser_create. WhenvendorDisplayNameis omitted, the transform must write it as an empty string so interpolation does not persist the literal{{vendorDisplayName}}token;user_createthen stores the email asdisplay_name. Workflows already instantiated from an older copy of this starter keep the previous transform until you re-instantiate from the gallery. - There is no generic locked-variable flag.
expiresInDayshas to be a workflow variable so the transform sandbox can read it (vars.expiresInDays); template-parameter substitution skipsscriptkeys on purpose (see.cursor/rules/server/sandbox-security.mdc). The instantiated number is the variable default. A catalog publisher who needs the field hidden from requesters should omit it from the published input form. Callers who can start the run can still override it via the start API.
Overview¶
flowchart TD
Start[Start]
Compute["Validate email and compute expiry"]
Provision["Provision or reuse vendor account"]
Grant["Grant vendor role with expiresAt"]
NotifyOk[Notify sponsor — granted]
NotifyFail[Notify sponsor — failed]
Success[End: Success]
Failure[End: Failure]
Start --> Compute
Compute -->|success| Provision
Compute -->|error| NotifyFail
Provision -->|success| Grant
Provision -->|error| NotifyFail
Grant -->|success| NotifyOk
Grant -->|error| NotifyFail
NotifyOk --> Success
NotifyFail --> Failure
Step 1 — Create the workflow¶
Navigate to Design in the sidebar (opens /workflows), open the
Workflows tab, and click New Workflow:
| Field | Value |
|---|---|
| Name | External Vendor Auto-Provision |
| Description | Provision a vendor user and grant a time-bound role with no approval gate |
| Category | general |
| Error Strategy | stop |
| Trigger | manual |
Leave Subject Variable unset. general workflows do not take a
subject.
Step 2 — Variables¶
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
vendorEmail |
string | yes | — | Vendor contact to provision or reuse. |
vendorDisplayName |
string | no | — | Used only when a new Floh user must be created. |
expiresInDays |
number | yes | (from template parameter) | Days until the granted role expires. |
Step 3 — Build the graph¶
- Validate input and compute access expiry (
transform) — reject a non-stringvendorEmail, a provided non-stringvendorDisplayName, a boolean/array/objectexpiresInDays, and a non-positiveexpiresInDays. Numeric strings such as"7"are accepted. Write omittedvendorDisplayNameas"", then writeaccessExpiresAtas an ISO timestamp (Date.parse(now()) + days * 24 * 60 * 60 * 1000). Readvars.expiresInDays,vars.vendorEmail, andvars.vendorDisplayName; do not interpolate those values into the script source.error→ failure notification. - Provision or reuse vendor account (
user_create) —email: "{{vendorEmail}}",ifExists: "reuse". ExposesuserCreateUserId/userCreateEmailon success.error→ failure notification. - Grant vendor role (
role_grant) —
| Config key | Value |
|---|---|
roleDefinitionId |
the vendor role |
userId |
{{userCreateUserId}} |
expiresAt |
{{accessExpiresAt}} |
onDuplicate |
error |
failOnPartial |
true |
There is no later role_revoke and no delay step. error →
failure notification.
- Notify sponsor that access is active (
notification) — internal recipient. The body should include{{accessExpiresAt}}so the sponsor knows when the assignment will expire. - Notify sponsor of provisioning failure (
notification) — same recipient; include{{lastStepError.stepId}}and{{lastStepError.message}}. - Two end nodes:
successandfailure.
Step 4 — Parameters (if you save this as a template)¶
| Parameter | Required | Purpose |
|---|---|---|
sponsorUserId |
yes | Notification contact on grant and on failure. Not an approver. |
vendorRoleId |
yes | Role granted for the access window. |
expiresInDays |
yes | Days until expiry. Substituted into the expiresInDays variable default. |
Step 5 — Connectors¶
This graph uses no connectors. Expiry is the role-assignment scheduler,
not the built-in delay connector.
Step 6 — Publish¶
Save, then Publish. Start a run with a test email, confirm a Floh
user is created (or reused), confirm the role assignment has an
expires_at timestamp, and confirm a second start for the same vendor
- role fails closed (
onDuplicate: "error") rather than silently skipping.