How-To: New Employee Onboarding Workflow¶
A step-by-step guide to building a workflow that lets a manager (or HR admin) request a new employee account. The workflow collects the candidate's name, manager, and personal email; verifies that the candidate actually controls that personal address with an OTP; then provisions the candidate in Floh, Active Directory, and Authifi before sending them a welcome email with a one-time password-setup link.
What you'll build¶
Submitter (HR / hiring manager) opens the request catalog
│
▼
Catalog form collects firstName, lastName, preferredName?, manager, personalEmail
│
▼
Start
│
▼
Verify Personal Email (verify_contact, mode: link_and_code)
│
├── success ─┐
│ ▼
│ Derive Account Details (transform — username, display name)
│ │
│ ▼
│ Create Floh Profile (user_create, setAsSubject)
│ │
│ ▼
│ Set Manager (profile_update, managerId)
│ │
│ ▼
│ Grant "New Employee" Role (role_grant — provisions AD + Authifi via entitlements)
│ │
│ ▼
│ Send Welcome Email + First Password Link (first_password_invitation)
│ │
│ ├── success ─┐
│ │ ▼
│ │ Swap Primary Email (profile_update, email)
│ │ │
│ │ ▼
│ │ End
│ │
│ └── expired ─► Notify IT — Password Link Expired ─► End
│
├── expired ─► Notify Submitter — Verification Expired ─► End
└── exhausted ► Notify Submitter — Too Many Attempts ─► End
Prerequisites¶
- Permissions on the submitter's account.
workflow:manageto author and publish the workflow.workflow:provision_usersfor any human who will start the workflow. Theuser_createstep gates on this permission on the run initiator, not on the workflow author. Without it, the run fails at stepCreate Floh Profilewithdenied_missing_permission.- Feature flags on. Set
FEATURE_VERIFY_CONTACT=trueandFEATURE_FIRST_PASSWORD=truein the environment (both default totruein dev/test,falsein production). WithoutFEATURE_FIRST_PASSWORDthefirst_password_invitationstep fails immediately at runtime. Confirm at/api/healthor with an admin who can check the env. - Authifi connector configured and passing
Test Connectionon the Connectors page. Note its connector name — you'll reference it in the Authifi entitlement definition. - Active Directory connector configured. Floh ships only the
simulated
test-activedirectoryconnector out of the box; for a real deployment you need a custom AD connector that exposescreateAccount,setPassword, anddisableAccount. This guide usestest-activedirectoryso the walkthrough is reproducible on a fresh dev install — the entitlement config for a real connector is structurally identical, just substitute the connector name. - Authifi group named
new-employee(or whatever your organization uses). Create it from the Authifi admin console and note its UUID — you'll paste it into the Authifi entitlement's provision config. - Entitlement definitions for AD and Authifi. Create these from the Entitlements page before building the workflow:
"Corporate AD Account" entitlement:
| Field | Value |
|---|---|
| Name | Corporate AD Account |
| Connector | test-activedirectory |
| Provision Config | { "command": "createAccount" } (username is derived from userEmail by the test connector) |
| Deprovision Config | { "command": "disableAccount" } |
| Reconciliation Config | { "command": "checkAccountExists" } |
"New Employee Authifi Group" entitlement:
| Field | Value |
|---|---|
| Name | New Employee Authifi Group |
| Connector | authifi |
| Provision Config | { "command": "addToGroup", "groupId": "<new-employee-group-uuid>", "role": "member", "createIfMissing": true } |
| Deprovision Config | { "command": "removeFromGroup", "groupId": "<new-employee-group-uuid>" } |
| Reconciliation Config | { "command": "checkGroupMembership", "groupId": "<new-employee-group-uuid>" } |
Replace <new-employee-group-uuid> with the actual Authifi group
UUID.
- "New Employee" role definition linking both entitlements above.
Navigate to Role Definitions → New Role, name it
New Employee, save, then click Link Entitlement to attach bothCorporate AD AccountandNew Employee Authifi Group. Note the role definition UUID — you'll reference it in therole_grantstep. - Email template (optional). A
welcome_employeeemail template in Templates → Email Templates lets you author the welcome email body once and reuse it across runs. This guide uses an inlinemessageTemplateonfirst_password_invitationto keep the walkthrough self-contained.
Step 1 — Create the workflow shell¶
- In the admin UI, navigate to Workflows and click New Workflow.
- Fill in the General tab:
| Field | Value |
|---|---|
| Name | New Employee Onboarding |
| Description | Provision a new hire in Floh, AD, and Authifi after personal-email verification. |
| Category | general |
| Subject Variable | (disabled — the general category does not allow one) |
| Error Strategy | stop |
| Trigger | manual |
Why
generaland notuser_self_service? The submitter (a manager or HR admin) is not the person being provisioned, souser_self_service(which auto-binds the workflow target to the submitter) is wrong.userworks only if the new employee already has a Floh user row — but here we want to verify their personal email before creating the Floh row, souser_createmid-run is the right tool. Theuser_createstep then opts intosetAsSubject: trueso the run still gets asubject_idonce the Floh user exists — the new hire shows up underGET /api/users/:id/workflow-activityfrom that point onwards.
- Save the draft. The designer drops you on the Variables tab.
Step 2 — Define context variables¶
Open the Variables tab and add the following rows. Click Add Variable for each one and fill in the fields shown:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
firstName |
String |
yes | — | Legal first name (used to derive AD username and welcome email salutation). |
lastName |
String |
yes | — | Legal last name. |
preferredName |
String |
no | — | Optional preferred / chosen name. Used in greetings if set. |
personalEmail |
String |
yes | — | The candidate's personal email — the only address we know is reachable before AD provisioning. Verified by step 4.1. |
manager |
User |
yes | {{submitter.id}} |
The new employee's manager. Defaults to the submitter (since 90% of the time HR submits on behalf of the manager — change in the form if not). |
Notes:
- The table above lists only variables that need to be declared up
front because they're submitter-supplied (the form fields and
manager). Other workflow variables —personalEmailVerified(from step 4.1),adUsername,displayName, andonboardingId(all from step 4.4),flohUserand friends (from step 4.5),roleAssignmentIdandroleProvisioned(from step 4.7) — appear in the run variable bag automatically as their producing step'soutputVariable/outputKey. You don't pre-declare them, and they're available to every step that runs after the producer. Note that{{run.id}}is not a usable template token: the workflow run id is not exposed in the variable bag, so a transform that needs a stable per-run identifier (e.g. for theinitialAttributes.onboardingIdcustom attribute below) generates one withfloh.uuid()instead. managerof typeUserlets the submitter pick a teammate from a typeahead picker on the catalog form. The picker is scope-restricted: non-admin submitters only see users they share at least one effective group membership with, plus themselves. Admins (user:readpermission) see the full directory.- The default
{{submitter.id}}onmanageronly resolves at run start (not in the catalog form preview). Submitters who want a different manager open the picker and pick the right teammate.
Step 3 — Customize the catalog form (Input Form tab)¶
Open the Input Form tab. The designer seeds a Handlebars scaffold that lists every variable in declaration order. That layout is acceptable, but for HR/onboarding it reads better grouped:
<div class="grid">
<div class="col-12">
<h3 class="mt-0">Personal information</h3>
</div>
<div class="col-12 md:col-6">{{firstName.label}} {{firstName}}</div>
<div class="col-12 md:col-6">{{lastName.label}} {{lastName}}</div>
<div class="col-12 md:col-6">{{preferredName.label}} {{preferredName}}</div>
<div class="col-12 md:col-6">{{personalEmail.label}} {{personalEmail}}</div>
<div class="col-12 mt-4">
<h3 class="mt-0">Reporting line</h3>
</div>
<div class="col-12">
<small class="text-color-secondary mb-2 block">
Pick the new hire's manager from the directory. Leave blank to default to your own account.
</small>
{{manager.label}}
{{manager}}
</div>
</div>
The renderer mounts a real input where each {{name}} token sits and
suppresses the built-in label/description because we render
{{name.label}} in the same block. The right-hand pane previews the
form live as you type.
If you'd rather skip the custom layout, leave the editor scaffold alone — you'll just get a flat field list in declaration order.
Step 4 — Add the workflow steps¶
In the Steps tab, click Add Step for each row below. The order in this list matches the natural execution order; transitions are wired in Step 5.
4.1 — Verify Personal Email (verify_contact)¶
| Config Field | Value |
|---|---|
| Step ID | verify-personal-email |
| Step Name | Verify Personal Email |
| Type | verify_contact |
| Recipient Email | {{personalEmail}} |
| Mode | link_and_code |
| Link Expires (h) | 24 |
| OTP Length | 6 |
| OTP Expires (min) | 10 |
| OTP Max Attempts | 5 |
| Output Variable | personalEmailVerified |
| Custom Subject | Confirm your email to start onboarding at Acme |
| Message Template | (leave default — the built-in template renders a styled "Verify" button using {{acceptUrl}} and the recipient's address) |
The candidate gets an email containing a high-entropy verification
link. Clicking it opens /verify on the public portal and a second
email arrives with the 6-digit code; they type the code on the page
to complete verification.
On success the variable bag now carries:
{
"personalEmailVerified": {
"verified": true,
"verifiedEmail": "candidate@gmail.com",
"verifiedAt": "2026-05-01T13:42:00.000Z",
"mode": "link_and_code"
}
}
4.2 — Notify Submitter — Verification Expired (notification)¶
This is the recovery branch for the expired outcome (link or code
TTL elapsed). Wire it via Step 5 — this
panel just defines the step.
| Config Field | Value |
|---|---|
| Step ID | notify-otp-expired |
| Step Name | Notify Submitter — Verification Expired |
| Type | notification |
| Recipient Type | Internal User |
| Recipient User | {{submitter.id}} |
| Subject Override | Onboarding paused — {{firstName}} {{lastName}} did not verify their email |
| Custom Body | The verification email sent to {{personalEmail}} expired before {{firstName}} clicked the link or entered the code. Resubmit the request once you have a current address for them. |
| Require Acceptance | unchecked |
4.3 — Notify Submitter — Too Many Attempts (notification)¶
The exhausted recovery branch. Same shape as 4.2 but with a
different message:
| Config Field | Value |
|---|---|
| Step ID | notify-otp-exhausted |
| Step Name | Notify Submitter — Too Many Attempts |
| Type | notification |
| Recipient Type | Internal User |
| Recipient User | {{submitter.id}} |
| Subject Override | Onboarding paused — {{firstName}} {{lastName}} entered an invalid code too many times |
| Custom Body | {{firstName}} entered the wrong verification code five times. The current request has been closed; please resubmit and confirm the address with them out-of-band first. |
| Require Acceptance | unchecked |
4.4 — Derive Account Details (transform)¶
Generate a stable AD username (e.g. flast) and a tracking
identifier from the verified inputs.
| Config Field | Value |
|---|---|
| Step ID | derive-account-details |
| Step Name | Derive Account Details |
| Type | transform |
| Outputs | adUsername, displayName, onboardingId |
| Script | see below |
var v = floh.variables;
// Username = first initial + last name, lowercased and ASCII-folded.
// Adjust to match your AD naming policy (e.g. "first.last").
//
// COLLISION RISK: this naive scheme produces the same username for
// "Jane Smith" and "James Smith" → both `jsmith`. Step 4.7's
// `role_grant` will fail on the second onboard with no recovery
// edge in the graph. For production, add a `connector` step BEFORE
// step 4.7 that calls `test-activedirectory.checkAccountExists` (or
// `findUser`) on the candidate username and, if taken, append a
// numeric suffix in a follow-up `transform` (`jsmith` → `jsmith2`).
// The walkthrough leaves this out so the happy-path graph stays
// compact — see the "Known limitations" section for the full story.
var first = String(v.firstName || "")
.trim()
.toLowerCase();
var last = String(v.lastName || "")
.trim()
.toLowerCase();
var ascii = function (s) {
return s.replace(/[^a-z0-9]/g, "");
};
var adUsername = (ascii(first).charAt(0) || "x") + (ascii(last) || "user");
// Display name uses preferred name when present, legal name otherwise.
var displayName =
v.preferredName && String(v.preferredName).trim()
? String(v.preferredName).trim() + " " + v.lastName
: v.firstName + " " + v.lastName;
// `floh.uuid()` is documented for non-cryptographic unique-identifier
// use (see `docs/workflows/transform-step-guide.md`). We use it to
// stamp every onboarding event with a stable id that downstream
// systems (offboarding workflow, audit log queries) can key off
// without depending on the run.id template token, which is NOT
// available in the workflow's variable bag.
var onboardingId = floh.uuid();
return {
adUsername: adUsername,
displayName: displayName,
onboardingId: onboardingId,
};
Click Test Script under the editor and paste sample variable values. Confirm the username and display-name shape, then save.
4.5 — Create Floh Profile (user_create)¶
| Config Field | Value |
|---|---|
| Step ID | create-floh-user |
| Step Name | Create Floh Profile |
| Type | user_create |
{{personalEmailVerified.verifiedEmail}} |
|
| Display Name | {{displayName}} |
| If Exists | fail (refuse to onboard the same person twice) |
| Set As Subject | ✅ (general workflow only; binds the run's subject_id to the new user once it exists, so the new hire shows up under GET /api/users/:id/workflow-activity) |
| Initial Attributes | see below |
| Output Key | flohUser |
Initial attributes (the JSON entry on the Initial Attributes field):
{
"preferredName": "{{preferredName}}",
"personalEmail": "{{personalEmailVerified.verifiedEmail}}",
"onboardingId": "{{onboardingId}}"
}
preferredNameempty-string behavior.preferredNameis an optional variable; when the submitter leaves it blank, the Handlebars template above renders"preferredName": ""(an empty string, not a missing key). Downstream consumers that want "preferred name was not provided" semantics should testattribute.preferredName?.length > 0(orBoolean(attribute.preferredName)) rather thanattribute.preferredName !== undefined— the key is always present onceuser_createwrites it. We accept this trade-off because theinitialAttributesJSON field is treated as a single Handlebars template at runtime; a{{#if preferredName}}…{{/if}}guard around the key would either leave a trailing comma in the rendered JSON (depending on key order) or require committing to a specific key-ordering hack, both of which are more brittle than documenting the empty-string default.
On success the bag carries:
{
"userCreated": true,
"userCreateUserId": "f4a8…",
"userCreateEmail": "candidate@gmail.com",
"flohUser": {
/* full payload */
}
}
4.6 — Set Manager (profile_update)¶
| Config Field | Value |
|---|---|
| Step ID | set-manager |
| Step Name | Set Manager |
| Type | profile_update |
| User ID | {{userCreateUserId}} |
| Manager ID | {{manager.id}} |
This writes the user row's first-class manager_id column so that
approver: "manager:{{user.id}}" resolves correctly on subsequent
runs. Cycles (A → B → A) and self-management are rejected at the
repository layer with a typed error; successful writes emit a
user.manager_changed audit row.
4.7 — Grant "New Employee" Role (role_grant)¶
| Config Field | Value |
|---|---|
| Step ID | grant-new-employee-role |
| Step Name | Grant "New Employee" Role |
| Type | role_grant |
| Role Definition ID | (UUID of the "New Employee" role from prerequisite 7) |
| User ID | {{userCreateUserId}} |
| Fail on Partial | false (unchecked — allows partial provisioning so the run continues) |
| On Duplicate | skip |
This single step provisions both external accounts via the entitlement system:
- Creates a
role_assignmentrecord for "New Employee" linked to this run. - For each entitlement on the role:
- Corporate AD Account → calls
test-activedirectory.createAccountwith the auto-injected user identity. Creates the AD account and establishes an external identity link in Floh. - New Employee Authifi Group → calls
authifi.addToGroupwith the auto-injected user identity. Creates the Authifi user (viacreateIfMissing: true) and adds them to thenew-employeegroup. - Marks the assignment as
active.
The step produces output variables: roleAssignmentId,
roleProvisioned, provisionedCount, failedCount.
Why entitlements instead of direct connector steps? Entitlement definitions carry both a
provision_configand adeprovision_config. When the Employee Offboarding workflow runsrole_revoke, the entitlement system automatically calls each deprovision command (disableAccount,removeFromGroup). This means deprovisioning logic is defined once and reused — no need to maintain parallel connector steps in the offboarding workflow.Test-connector note. The
test-activedirectoryconnector requires an explicitusernameparameter oncreateAccount. The entitlement auto-injection providesuserEmailanduserDisplayNamefrom the Floh user row. The test connector'sensureUsernameFromEmailhook auto-derivesusernamefromuserEmail's local part at runtime when no explicitusernameis provided in the config. In production, a real AD connector derives the SAM account name from the provided email or UPN directly.
4.8 — Send Welcome Email + First Password Link (first_password_invitation)¶
The first_password_invitation step replaces the old pattern of
generating a temporary password in a transform step and
interpolating it into a notification body. Instead, it issues a
one-shot HMAC-peppered link the new hire clicks to choose their own
password — the plaintext never traverses email or the run variable
bag. The step's own messageTemplate doubles as the welcome email,
so a separate notification step is not needed.
| Config Field | Value |
|---|---|
| Step ID | first-password-invitation |
| Step Name | Send Welcome Email + First Password Link |
| Type | first_password_invitation |
| Connector ID | test-activedirectory (or your real AD connector instance) |
| User Key | {{adUsername}} |
| Recipient Email | {{personalEmailVerified.verifiedEmail}} |
| Expires (h) | 48 |
| Custom Subject | Welcome to Acme — set your password to get started |
| Message Template | see below |
| Output Variable | firstPasswordSet |
Message template:
Hi {{#if preferredName}}{{preferredName}}{{else}}{{firstName}}{{/if}},
Welcome to Acme! Your accounts are ready. Set your password to
get started:
Username: {{adUsername}}
Set your password: {{firstPasswordUrl}}
This link expires at {{firstPasswordExpiresAt}}. After setting
your password, sign in at https://login.corp.example.com.
Your manager ({{manager.displayName}}) will reach out about
team-specific onboarding.
Welcome aboard,
The Acme IT team
The step pauses at waiting_first_password_set. When the new hire
clicks the link, the portal page at /first-password lets them
choose a password; on submit the public route calls the AD
connector's setPassword command. A connector_rejected (password
too short, for example) does not fail the step — the invitation
stays pending so the user can retry. On success the bag carries:
{
"firstPasswordSet": {
"passwordSet": true,
"connectorId": "test-activedirectory",
"userKey": "jdoe",
"acceptedAt": "2026-05-02T14:30:00.000Z"
}
}
See First Password Invitation for the full contract and security model.
4.9 — Swap Primary Email (profile_update)¶
After provisioning completes, rotate the Floh user's primary email
from the personal address (used at sign-up) to the corporate AD
address. This ensures {{user.email}} resolves to the corporate
address in downstream workflows.
| Config Field | Value |
|---|---|
| Step ID | swap-primary-email |
| Step Name | Swap Primary Email |
| Type | profile_update |
| User ID | {{userCreateUserId}} |
{{adUsername}}@corp.example.com (replace corp.example.com with your AD primary domain; adUsername is derived in step 4.4 using the same local-part logic the test connector uses internally) |
|
| Require Reconfirm | false (set to true and add a follow-up verify_contact step if you need the corporate address re-verified before the user is fully confirmed) |
The UNIQUE(iss, email, deleted_at) index is honoured — collisions
surface a typed EMAIL_IN_USE step error. Successful writes emit a
user.email_changed audit row.
4.10 — Notify IT — Password Link Expired (notification)¶
If the new hire doesn't set their password before the invitation expires, this notification alerts IT so they can reissue the onboarding or resend the invitation manually.
| Config Field | Value |
|---|---|
| Step ID | notify-password-link-expired |
| Step Name | Notify IT — Password Link Expired |
| Type | notification |
| Recipient Type | Internal User (or a team mailbox, per your ops model) |
| Recipient User | use the autocomplete to pick the IT on-call user or distribution group |
| Subject Override | Onboarding paused — password setup link expired for {{firstName}} {{lastName}} |
| Custom Body | The first-password invitation expired before {{firstName}} {{lastName}} completed it. Reissue the onboarding workflow or resend the invitation from the run detail page. |
| Require Acceptance | unchecked |
4.11 — End¶
| Config Field | Value |
|---|---|
| Step ID | end |
| Step Name | End |
| Type | end |
The start step is created automatically when the workflow is saved —
you don't need to add it manually.
Step 5 — Wire the graph¶
Open the Graph tab and draw the following transitions. Each click
in the editor goes from a step's output port (right side) to the
next step's input port (left side). Multi-output steps
(verify_contact here) prompt for a transition label after the second
or later draw.
| # | From | Edge label | To |
|---|---|---|---|
| 1 | Start | success |
Verify Personal Email |
| 2 | Verify Personal Email | success |
Derive Account Details |
| 3 | Verify Personal Email | expired |
Notify Submitter — Verification Expired |
| 4 | Verify Personal Email | exhausted |
Notify Submitter — Too Many Attempts |
| 5 | Derive Account Details | success |
Create Floh Profile |
| 6 | Create Floh Profile | success |
Set Manager |
| 7 | Set Manager | success |
Grant "New Employee" Role |
| 8 | Grant "New Employee" Role | success |
Send Welcome Email + First Password Link |
| 9 | Send Welcome Email + First Password Link | success |
Swap Primary Email |
| 10 | Send Welcome Email + First Password Link | expired |
Notify IT — Password Link Expired |
| 11 | Swap Primary Email | success |
End |
| 12 | Notify Submitter — Verification Expired | success |
End |
| 13 | Notify Submitter — Too Many Attempts | success |
End |
| 14 | Notify IT — Password Link Expired | success |
End |
Tip: if you don't wire the
expiredandexhaustededges explicitly, the engine still recovers — it falls through toon: "error"and then to the workflow's globalonErrorpolicy (stopin our case). The run won't hang, but the submitter never hears why; the explicit notification edges in 3 and 4 are worth the two extra steps.Recommended (not shown above): add an
on: "error"edge from therole_grantstep (4.7) and the email-swap step (4.9, which can surfaceEMAIL_IN_USE) to a dedicated "Provisioning Failed — admin attention required" notification → End. The defaultonError: stopstrategy stops the run silently otherwise, leaving partial state across Floh / AD / Authifi.The Provisioning Chain — with failure routing template (Templates → Insert from Template → "Workflow Definition") gives you a working starter graph with these edges already wired. The workflow designer surfaces a yellow lint banner (
PROVISIONING_CHAIN_NO_ERROR_EDGE) whenever a chain of connector orrole_grantsteps lacks expliciton: "error"edges. See the Partial Provisioning Runbook for cleanup steps when a partial-provisioning failure does happen.
Step 6 — Save and validate¶
Click Save Draft. The designer runs:
- Static validation — duplicate step IDs, unreachable nodes, missing required config fields.
- Role reference validation — the
roleDefinitionIdon therole_grantstep resolves to an existing role definition. - Lint warning — if you skipped the recommended
on: "error"edge onrole_grant, you'll see a yellowPROVISIONING_CHAIN_NO_ERROR_EDGElint banner.
Step 7 — Test Run before publishing¶
- From the workflow detail page, click Test Run.
- Fill in the form. Use your own email for
personalEmailso you can complete the OTP loop end-to-end. Pick the candidate's manager from the Manager typeahead (start typing a name, email, or username — the picker is scoped to users you can see) or leave the field blank to default to{{submitter.id}}(you). - Click Start Run.
- The run pauses at
Verify Personal Emailwith statuswaiting_verification. Check the email at the address you used, click the link, and enter the 6-digit code on/verify. - After verification, the provisioning steps (
Derive Account Details→Create Floh Profile→Set Manager→Grant "New Employee" Role) execute automatically. Watch the Runs tab — each should reachsucceededwithin seconds. - The run pauses again at
Send Welcome Email + First Password Linkwith statuswaiting_first_password_set. Check the email you used — it contains the welcome message and a one-time password-setup link. Click the link, set a password on the/first-passwordportal page, and submit. - The run resumes.
Swap Primary Email→Endcompletes automatically. - Confirm:
- Floh user — the new user appears under Users with the
personal email and the custom attributes from
initialAttributes. The manager column shows the selected manager. - Role assignment — the Role Assignments page shows an
activeassignment for "New Employee" linked to this run. - AD account — open the
test-activedirectoryconnector's Commands tab and runfindUserwith the derived username. Runauthenticatewith the password you set to verify it took. - Authifi membership — run
checkGroupMembershipon the Authifi connector with the new userId and thenew-employeegroup. - Primary email — the user's primary email should now be the corporate AD address (from step 4.9).
Step 8 — Publish and add to the catalog¶
- Click Publish on the workflow detail page.
- Open the Catalog tab and configure:
| Field | Value |
|---|---|
| Published | checked |
| Icon | person_add |
| Description | Provision a new employee in Floh, AD, and Authifi after personal-email verification. |
| Tags | hr, onboarding, provisioning |
| Submission Groups | restrict to hr-admins and engineering-managers (don't leave open to all authenticated users — workflow:provision_users is a sensitive permission) |
Catalog changes auto-save after a short debounce.
Runtime walkthrough¶
- Manager opens the catalog and picks New Employee Onboarding.
- Manager fills in the form — first/last/preferred name, personal
email, and the candidate's manager via the Manager typeahead
picker (or leaves the field blank to default to themselves via
{{submitter.id}}). They click Submit. - OTP email lands in the candidate's inbox. They click the link;
/verifyissues a code that lands in their inbox in a second email. - Candidate enters the code on
/verify. The run resumes. - Engine derives
adUsername(jdoe),displayName(Jane Doe), andonboardingId. - Floh user row is created with the personal email as
emailand the custom attributes frominitialAttributes. - Manager is linked — the
profile_updatestep writesmanager_idon the Floh user row. - "New Employee" role is granted — the
role_grantstep provisions both entitlements: - AD entitlement →
createAccountcreatesjdoe@corp.example.com. - Authifi entitlement →
addToGroupcreates the user in the target tenant (viacreateIfMissing: true) and adds them tonew-employee. Both entitlement instances are markedprovisioned; the role assignment is markedactive. - Welcome email + password link lands at the personal address.
The run pauses at
waiting_first_password_set. - Candidate sets their password by clicking the link and
choosing a password on the
/first-passwordportal page. The AD connector'ssetPasswordcommand fires server-side. The run resumes. - Primary email is swapped from the personal address to the
corporate AD address via
profile_update. - Run completes. The submitter sees
Completedon the run detail page and can drill into each step's variables.
Historical context¶
This walkthrough was originally drafted under epic LSA-8544 while the building blocks it exercises were still under construction. Eight gaps were tracked as individual stories (LSA-8652 through LSA-8659) covering subject linkage, in-portal user picker, manager linkage, test-AD password parity, first-password invitation, email rotation, failure-routing scaffold, and cluster-wide resend cooldown. All eight have shipped; the walkthrough above uses them directly with no workarounds.
Known limitations¶
These are deliberate scope cuts in this walkthrough rather than shortcomings in the underlying Floh building blocks. Each is fixable inside the workflow today by adding extra steps; the walkthrough omits them only so the happy-path graph stays compact.
- No AD username collision handling. The step 4.4 transform
produces
<first-initial><last-name>and the entitlement system forwards it tocreateAccount. "Jane Smith" and "James Smith" both resolve tojsmith, and the second onboard fails at therole_grantstep. To handle this in production, insert aconnectorstep between 4.4 and 4.7 that callstest-activedirectory.checkAccountExists(orfindUser) on the candidate username, route theexists: trueedge into a follow-uptransformthat appends a numeric suffix (jsmith→jsmith2→jsmith3…), and loop until the check returnsexists: false. The happy path then continues torole_grantas written. - No
on: "error"edges on provisioning / profile steps. The walkthrough wires only theverify_contactandfirst_password_invitationfailure edges. Therole_grantstep (4.7) and the email-swap step (4.9, which can surfaceEMAIL_IN_USE) fall through to the workflow'sonError: "stop"policy on failure, leaving the run infailedand the side-effects in a half-completed state. For production, add anon: "error"edge from each of these steps to a singleProvisioning Failed — Admin Attention Requirednotification step that pages the IT operations group. - Test-connector simplification. The
test-activedirectoryconnector normally requires an explicitusernameoncreateAccount, but itsensureUsernameFromEmailhook auto-derives it from the email local part, so the walkthrough succeeds without extra config. A production real-AD connector typically derives the SAM account name from the provided UPN directly — verify your connector's parameter contract before promoting to production. - Authifi provisioning timing. With the entitlement-based approach,
addToGroupruns as part ofrole_grant(step 4.7), which executes before the user sets their first password (step 4.8). In the original direct-connector design,addToGroupran after password setup. If your Authifi group grants immediate access to resources that require a password, the user briefly has group membership without a usable credential. In practice this is acceptable because thefirst_password_invitationstep follows immediately, but note the ordering difference if your security model requires credentials before group assignment.
What to do next¶
- Run the companion Employee Offboarding
workflow to reverse provisioning —
role_revokeautomatically calls the entitlement deprovision commands (disableAccount,removeFromGroup) without any direct connector steps. - Subscribe the IT operations group to the workflow's runs feed
(
GET /api/runs?workflowId=...&status=failed) so partial provisioning surfaces fast. - Use the Provisioning Chain — with failure routing template
(Templates → Insert from Template → "Workflow Definition") to
scaffold
on: "error"edges and a cleanup notification for therole_grantstep. - Create entitlement definitions for additional connectors (Google Workspace, SCIM providers) so their provisioning and deprovisioning are also automatic via role grant/revoke.
References¶
- Employee Offboarding workflow — the
companion deprovisioning guide using
role_revoke. - Verify Contact step — full config and security model for the OTP step used in 4.1.
- User self-service workflows — covers the
user_self_servicecategory we deliberately don't use here, plus thesubmittersnapshot semantics. - Provisioning workflow with role lifecycle — document-driven provisioning with automatic expiry revocation.
- Roles & Entitlements —
role definitions, entitlement definitions, and the provision/deprovision
model used by the
role_grantstep. - Project Onboarding workflow — same
Authifi
addToGrouppattern in a project-membership context. - User profiles and
profile_update/user_create— schema forinitialAttributesandattributesFrom. - Floh workflows authoring rules — step-type reference, validation invariants, and the anti-patterns list.