Skip to content

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

  1. A vendor role the contact should receive for the access window, created under People → Roles.
  2. The workflow:create, workflow:update, workflow:publish, and workflow:read permissions on your admin account (the Workflows tab and GET /api/workflows are gated on workflow:read), and template:read if you instantiate the starter from the gallery. The account that runs this workflow also needs workflow:start and workflow:provision_users — user_create refuses to insert an unconfirmed user without the latter, and every outcome is captured in a workflow.user_auto_provisioned audit row.

Security model — read this before publishing

  • Do not wait out the access window with the delay connector. delay is an in-process setTimeout. A day-scale wait keeps the run running until stuck-run recovery (default 30 minutes) fails it — leaving the grant in place with no revoke. Use role_grant.expiresAt and let the role-expiry-check scheduler expire the assignment. See the Time-Bound Group Access starter for the same pattern.
  • Fail closed on duplicate grants. role_revoke and the expiry job key assignments by user+role, not by "the assignment this run created". Default onDuplicate: "skip" would let a second run succeed and a later expiry would tear down a pre-existing assignment this workflow never owned. Set onDuplicate: "error".
  • Fail closed on partial entitlement provision. findExpired() only selects active assignments. Default failOnPartial: false leaves a partially_provisioned row whose surviving entitlements never expire. Set failOnPartial: true.
  • Validate duration, email, and display name before provisioning. A zero/negative expiresInDays, a boolean/array/object expiresInDays (Number(true) === 1 and Number([365]) === 365 would otherwise provision), a non-string vendorEmail, or a provided non-string vendorDisplayName (start payloads type variables as unknown; 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 before user_create. When vendorDisplayName is omitted, the transform must write it as an empty string so interpolation does not persist the literal {{vendorDisplayName}} token; user_create then stores the email as display_name. Workflows already instantiated from an older copy of this starter keep the previous transform until you re-instantiate from the gallery.
  • The access duration is fixed per deployed workflow. expiresInDays is a parameter of the transform step (config.params.expiresInDays), not a workflow variable. Instantiation fills it from the template parameter, and the script reads it as floh.params.expiresInDays. Template substitution still never touches script itself (see .cursor/rules/server/sandbox-security.mdc). Because step config is part of the stored definition, nobody starting a run can change it; an expiresInDays sent in a start request is ignored. To change the duration, re-instantiate the template or edit the step's params (workflow export/import or MCP update_workflow); the designer does not edit params yet.
  • Existing workflows are migrated. Migration 20260925150000_move_vendor_expires_in_days_to_step_params moves the duration into the step's params and removes the variable, for the template and for every workflow instantiated from it (including later versions). Only workflows still in the starter's shape are rewritten: after the starter's vars.expiresInDays read line is replaced, nothing in the steps, input form, or trigger may mention expiresInDays apart from the starter's own error message. Any other mention (a customized read, an {{expiresInDays}} token, a form field, even a comment) leaves the workflow unchanged and still reading the variable; update those by hand.

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.

The access duration is not a variable: it lives in the transform step's params (see Step 3).

Step 3 — Build the graph

  1. Validate input and compute access expiry (transform) — reject a non-string vendorEmail, a provided non-string vendorDisplayName, a boolean/array/object expiresInDays, and a non-positive expiresInDays. Numeric strings such as "7" are accepted. Write omitted vendorDisplayName as "", then write accessExpiresAt as an ISO timestamp (Date.parse(now()) + days * 24 * 60 * 60 * 1000). Read the duration from floh.params.expiresInDays (the step's config.params, filled at instantiation) and the vendor fields from vars.vendorEmail and vars.vendorDisplayName; do not interpolate any of them into the script source. error → failure notification.
  2. Provision or reuse vendor account (user_create) — email: "{{vendorEmail}}", ifExists: "reuse". Exposes userCreateUserId / userCreateEmail on success. error → failure notification. Reuse requires an active Floh user; an inactive match fails closed and follows this error edge without reactivating the account.
  3. 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.

  1. Notify sponsor that access is active (notification) — internal recipient. The body should include {{accessExpiresAt}} so the sponsor knows when the assignment will expire.
  2. Notify sponsor of provisioning failure (notification) — same recipient; include {{lastStepError.stepId}} and {{lastStepError.message}}.
  3. Two end nodes: success and failure.

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.