Google Workspace Password Reset Workflow¶
A step-by-step guide to four related self-service Google Workspace workflows:
- Self-service password reset — an authenticated Floh user resets the password on their own linked Google Workspace account.
- Set initial password — an admin or onboarding workflow lets a newly provisioned user choose their first password instead of receiving one in email.
- Approval-gated password reset — same as self-service, but the change is gated by a fixed approver group and produces audit notifications to the requestor and the security team.
- Manager-or-helpdesk approval — variant of the approval-gated flow that routes the task to the requestor's manager when one is configured and falls back to the helpdesk group otherwise. Useful when not every user has a manager on file.
Both flows use the connector's setPassword command, which applies a caller-supplied password (in contrast to resetPassword and rotatePassword, which generate a random value server-side). For the full command reference, see Google Workspace Connector — Password Management.
Prerequisites¶
- A Google Workspace connector configured and passing the test connection — see Configuring a Google Workspace Connector.
- The submitter must have an external identity link to a Google Workspace user. New accounts created via Google Workspace Account Request are linked automatically; otherwise enable connector sync (Email match strategy) so the link is created during the next sync pass. Without a link, the workflow has no way to know which Google account belongs to the requestor.
- The
workflow:managepermission on your admin account.
Security model — read this before publishing¶
setPassword writes the supplied value directly to Google. The trust anchor for self-service is the submitter's authenticated Floh session: anyone who can sign in to Floh as user X and submit this workflow can change the Google password for X's linked Google account.
That model is fine when:
- Your Floh sign-in is itself protected by SSO + MFA.
- Floh is not a downstream consumer of the Google account being reset (otherwise an attacker with the Floh session could lock the user out by changing the Google password and never delivering the new value).
If either assumption fails, gate the workflow behind an additional verification step. Options:
- An
approvalstep that routes to the user's manager. - An out-of-band OTP via a separate connector.
- Step-up authentication — set
requireStepUpAuthon the consent step (orcatalogRequireStepUpAuthon the catalog entry) so the submitter must complete an MFA challenge in their current Floh session before the password can be applied. See Security › Step-Up Authentication for the verification model and configuration.
The workflow shipped here intentionally does not enable any of those by default — add one before publishing in any environment where the assumptions above don't hold.
The newPassword variable below is marked secret: true. That has two effects:
- The portal renders the field as a masked password input (visible to the submitter, not echoed in plain text on screen).
- The value is encrypted at rest in the variable store.
The setPassword connector step is also explicitly designed so the password value is never promoted to a workflow output variable or written to run logs — only userKey, passwordSet, and changePasswordAtNextLogin flow downstream.
Workflow A — Self-Service Password Reset¶
Overview¶
flowchart TD
Submit[Portal Submit]
Start[Start]
Resolve["Resolve Target Email (transform)"]
SetPwd[Set Password connector]
Notify[Notify Success notification]
Done[End]
Submit --> Start --> Resolve --> SetPwd --> Notify --> Done
Step 1 — Create the workflow¶
Navigate to Workflows > New Workflow and fill in:
| Field | Value |
|---|---|
| Name | Google Workspace Password Reset |
| Description | Self-service password reset for the submitter's Google account |
| Category | user |
| Subject Variable | requestor |
| Error Strategy | stop |
| Trigger | manual |
Step 2 — Define variables¶
Open the Variables tab and add:
| Name | Type | Required | Secret | Default | Description |
|---|---|---|---|---|---|
requestor |
Requestor | yes | no | — | The submitting user (auto-filled from session, hidden in form) |
newPassword |
String |
yes | yes | — | The new Google Workspace password (8-100 characters) |
The Requestor preset binds the variable to the authenticated caller (type: "user" + selfPopulate: true). The portal hides it and the server overrides any caller-supplied value with the authenticated identity, so the submitter cannot reset someone else's password by hand-crafting a request.
Marking newPassword as Secret does two things: the portal form renders it as a masked password input, and the value is encrypted at rest. It is still passed to the setPassword step at run time via variable interpolation.
Step 3 — Add the workflow steps¶
3.1 — Start¶
Type: start. Entry point.
3.2 — Resolve Target Email¶
Type: transform.
| Config Field | Value |
|---|---|
| Script | See below |
| Outputs | targetEmail |
Script:
const requestor = floh.variables.requestor;
// Prefer an external identity link to the Google connector — that's the
// authoritative mapping. Fall back to the requestor's primary Floh email if
// you've configured the connector with sync + Email match strategy and
// confirmed the addresses match.
const link = (requestor.externalIdentities || []).find(
(identity) => identity.connectorName === "google-workspace-prod",
);
if (!link) {
throw new Error("No linked Google Workspace identity found for this user. Contact IT.");
}
return { targetEmail: link.externalEmail || requestor.email };
Replace google-workspace-prod with the instance name of your Google Workspace connector. Use the Test Script panel under the transform step to dry-run with a mock requestor payload.
3.3 — Set Password¶
Type: connector.
| Config Field | Value |
|---|---|
| Connector | your Google Workspace connector instance |
| Command | setPassword |
| userKey | {{targetEmail}} |
| password | {{newPassword}} |
| changePasswordAtNextLogin | false |
| Output Key | passwordReset |
Output variables (promoted flat into the workflow namespace): userKey, passwordSet, changePasswordAtNextLogin. The password value is intentionally not in the output set.
3.4 — Notify Success¶
Type: notification.
| Config Field | Value |
|---|---|
| Recipient Type | internal |
| Recipient User | {{requestor.id}} |
| Subject Override | Your Google Workspace password was changed |
| Custom Body | The password on {{userKey}} was just changed. If this wasn't you, contact IT immediately. |
Do not interpolate {{newPassword}} here. The notification body is persisted in run logs and delivered over email — neither is a safe channel for the cleartext password the user just chose.
3.5 — End¶
Type: end. Exit point.
Step 4 — Wire the graph¶
In the Graph tab, draw:
- Start → Resolve Target Email
- Resolve Target Email → Set Password
- Set Password → Notify Success
- Notify Success → End
Step 5 — Save, test, publish¶
- Click Save. Fix any validation errors the designer reports.
- Click Test Run and submit with a real
newPasswordvalue. Therequestorfield is hidden — the engine fills it in from your session. Verify the run completes and that you can sign in to Google with the new password. - Click Publish.
- On the Catalog tab, set Published, pick an icon (e.g.
lock_reset), add a clear description and any submission-group restrictions you want to apply.
What users see¶
When the workflow is published to the catalog:
- The user opens Request Catalog in the portal and clicks the password-reset card.
- A form appears with a single visible field —
newPassword— rendered as a masked input. Therequestorfield is invisible (server-bound). - On submit, the workflow runs synchronously through the steps above and the user receives the success notification within seconds.
Workflow B — Set Initial Password (admin onboarding variant)¶
This variant is a small modification of the Google Workspace Account Request flow. Instead of generating a random initial password and emailing it to the new hire, the requestor (typically the new hire's manager or the new hire themselves) supplies the password during the onboarding form. The user can sign in with a known value on day one, and you can avoid first-login forced change by adding the follow-up setPassword step described below.
To adapt the account-request workflow:
- Add a
newPasswordvariable (String, required, secret) to the workflow's variables. - Replace the
passwordargument on theCreate Google Account(createUser) step: - Old:
{{initialPassword}}(the value generated in the transform step). - New:
{{newPassword}}(the value the form captured). - Drop
initialPasswordfrom the transform step's outputs — it's no longer used and would otherwise leak a generated value into run logs. - Append a Set Password step (type:
connector, command:setPassword) right afterLink Google Identity. This step is required to deliver the "no forced change at first login" promise — thecreateUserAPI force-setschangePasswordAtNextLogin: trueregardless of input, and only a follow-upsetPasswordcall withchangePasswordAtNextLogin: falseclears it. Configure:
| Config Field | Value |
|---|---|
| Connector | your Google Workspace connector instance |
| Command | setPassword |
| userKey | {{primaryEmail}} |
| password | {{newPassword}} |
| changePasswordAtNextLogin | false |
- Update the success notification body so it does not include the password (the requestor already knows it).
The result: the user receives "your account is ready, sign in with the password you chose" instead of a transient generated value delivered over email.
Workflow C — Manager-Approved Password Reset¶
This is the recommended pattern for any environment where the security model assumptions for Workflow A don't hold — for example, when Floh sign-in is not protected by SSO + MFA, or when the Google account being reset is itself an upstream sign-in source.
The flow:
- Submitter chooses a new password and (optionally) provides a justification.
- An approval task is created for a fixed approver group. The submitter's password sits encrypted in the run's variable store until the task is resolved.
- On approval, the password is applied via
setPassword, the requestor is notified, and a separate audit notification is fanned out to a security/IT inbox. - On rejection, the requestor is notified that no change was made and the run ends.
The approval step prevents an attacker who has only briefly compromised a Floh session from silently rotating someone's Google password — the change requires an out-of-band human action by a third party.
Overview¶
flowchart TD
Submit[Portal Submit]
Start[Start]
Resolve["Resolve Target Email (transform)"]
Approve[Approval Gate]
SetPwd[Set Password connector]
NotifyOK[Notify Requestor success]
NotifyAudit[Notify Security audit]
NotifyReject[Notify Requestor rejected]
Done[End]
Submit --> Start --> Resolve --> Approve
Approve -->|approved| SetPwd
SetPwd --> NotifyOK
SetPwd --> NotifyAudit
NotifyOK --> Done
NotifyAudit --> Done
Approve -->|rejected| NotifyReject --> Done
Prerequisites specific to this variant¶
In addition to the shared prerequisites:
- A group named
google-password-reset-approvers(or similar) with at least one member. Create it under Groups in the admin sidebar. This group will receive the approval tasks. - (Optional) A group named
it-security-audit(or similar) to receive the audit notifications described in step 3.7. If you don't want a separate audit channel, omit step 3.7 and the corresponding graph edge.
Step 1 — Create the workflow¶
Navigate to Workflows > New Workflow and fill in:
| Field | Value |
|---|---|
| Name | Google Workspace Password Reset (Approved) |
| Description | Self-service password reset for the submitter's Google account, gated by approval |
| Category | user |
| Subject Variable | requestor |
| Error Strategy | stop |
| Trigger | manual |
Step 2 — Define variables¶
Open the Variables tab and add:
| Name | Type | Required | Secret | Default | Description |
|---|---|---|---|---|---|
requestor |
Requestor | yes | no | — | The submitting user (auto-filled from session, hidden in form) |
newPassword |
String |
yes | yes | — | The new Google Workspace password (8-100 characters) |
justification |
String |
no | no | — | Reason for the reset, shown to approvers (e.g. "lost my YubiKey") |
The justification value is not secret — it appears in the approval task UI and in the rejection/audit notification bodies. Don't put anything sensitive in this field.
The newPassword value remains encrypted at rest in the variable store while the approval task is open. It is only handed to Google after the approver clicks Approve.
Step 3 — Add the workflow steps¶
3.1 — Start¶
Type: start. Entry point.
3.2 — Resolve Target Email¶
Identical to Workflow A § 3.2. Reuse the same transform script — it produces a targetEmail output that the rest of the steps reference.
3.3 — Approval Gate¶
Type: approval.
| Config Field | Value |
|---|---|
| Approvers | group:google-password-reset-approvers |
| Due Date | (optional, ISO date if you want a deadline) |
Transitions:
success→ Set Passworderror→ Notify — Rejected
The task that appears in the approvers' inbox is titled with the workflow name and the step name (Google Workspace Password Reset (Approved) — Approval Gate); the requestor ({{requestor.email}}), justification, and any other workflow variables are visible in the task detail view. There is no separate "task body" config field — the approver sees the workflow context directly.
Approver guidance: approvers are vouching for the identity of the requestor, not for the password value (which they cannot see). The expected verification is an out-of-band signal — a Slack/Teams ping, a phone call, an in-person check — that the person who submitted the request is who they claim to be. Treat any "I lost my password and can't reach me on Slack" request with suspicion; that's the textbook account-takeover pattern.
3.4 — Set Password¶
Identical to Workflow A § 3.3. Same setPassword call against {{targetEmail}} with {{newPassword}}. The password value never leaves the variable store unencrypted until this step runs.
3.5 — Notify Requestor — Success¶
Type: notification.
| Config Field | Value |
|---|---|
| Recipient Type | internal |
| Recipient User | {{requestor.id}} |
| Subject Override | Your Google Workspace password was changed (approved) |
| Custom Body | Your password reset on {{userKey}} was approved and applied. If you did not initiate this request, contact IT immediately. |
Do not interpolate {{newPassword}} into the body. The submitter already chose the value; the notification persists in run logs and email and is not a safe channel for it.
3.6 — Notify Requestor — Rejected¶
Type: notification.
| Config Field | Value |
|---|---|
| Recipient Type | internal |
| Recipient User | {{requestor.id}} |
| Subject Override | Google Workspace password reset not approved |
| Custom Body | Your password reset request for {{targetEmail}} was not approved. The Google password has not been changed. If you still need access, contact IT or open a new request with more context. |
3.7 — Notify Security — Audit (optional)¶
Type: notification. Skip this step (and the matching graph edge in Step 4) if you don't have a dedicated audit channel.
| Config Field | Value |
|---|---|
| Recipient Type | group |
| Recipient Group | group:it-security-audit |
| Subject Override | [AUDIT] Google password reset applied for {{targetEmail}} |
| Custom Body | Requestor: {{requestor.email}} ({{requestor.id}})<br>Target Google account: {{targetEmail}}<br>Justification: {{justification}}<br>The password value itself is not stored or transmitted in this notification. See the run detail page in Floh for the full timeline including approver identity. |
This step gives the security team an immutable record (in the email/notification system and in Floh's run history) that a password reset happened, who initiated it, and who approved it. It does not include the password value.
3.8 — End¶
Type: end. Exit point.
Step 4 — Wire the graph¶
In the Graph tab, draw these transitions:
- Start → Resolve Target Email
- Resolve Target Email → Approval Gate
- Approval Gate →
success→ Set Password - Approval Gate →
error→ Notify Requestor — Rejected - Set Password → Notify Requestor — Success
- Set Password → Notify Security — Audit (omit if you skipped step 3.7)
- Notify Requestor — Success → End
- Notify Security — Audit → End (omit if you skipped step 3.7)
- Notify Requestor — Rejected → End
Steps 5 and 6 both fire on the same success edge from Set Password — this is a fan-out: the engine runs both notification steps in parallel and waits for both before proceeding to End.
Step 5 — Save, test, publish¶
- Click Save. Fix any validation errors the designer reports — the orgUnitPath validation does not apply here, but missing approvers or empty notification bodies will block save.
- Click Test Run. Submit with a real
newPasswordand ajustificationlike "test run". The run pauses at Approval Gate with a task in the approvers' inbox. - Sign in as a member of
google-password-reset-approvers(or use the Tasks view as an admin) and approve the task. The remaining steps complete and you should receive both notifications (success + audit). - Verify you can sign in to Google with the new password.
- Test the rejection path with a second submission — submit, then Reject the task. Confirm only the rejection notification fires and that the Google password is unchanged.
- Click Publish.
- On the Catalog tab, set Published, pick an icon (e.g.
lock_reset), add a clear description that mentions the approval requirement, and restrict submission to the right groups if applicable.
Routing approvals to the requestor's manager (alternative)¶
If you want approvals to route to each requestor's manager instead of a fixed group, you have two options depending on whether every user is guaranteed to have a manager configured:
- All users have managers (strict): replace the
Approversvalue in step 3.3 withmanager:{{requestor.id}}. This is a first-class engine primitive that readsmanager_idfrom the user record at approval-creation time. If the requestor has no manager on file, the approval step fails withNo approvals could be created — unresolved approver(s)and the run aborts. - Some users may not have a manager (graceful fallback): use Workflow C2 below, which routes to the manager when one exists and falls back to a helpdesk group otherwise.
The rest of the flow is unchanged.
Optional hardening: layer step-up authentication¶
The approval step is the strongest gate, but you can stack step-up authentication on top of it for environments with very high blast radius. Set requireStepUpAuth: true on the approval step's task to force the approver to complete an MFA challenge in their current Floh session before they can click Approve — this prevents an attacker who has only a stolen approver session cookie from rubber-stamping a malicious request.
Workflow C2 — Manager-or-Helpdesk Approval¶
A practical variant of Workflow C for organizations where not every user has a manager configured (contractors, executives, vendors, newly imported accounts where the manager link hasn't been resolved yet, etc.).
The routing rule is EITHER the requestor's manager OR a member of the helpdesk approver group, never both:
- If
requestor.manageris set → the approval task is assigned to the manager only. - If
requestor.manageris null → the approval task falls back to the helpdesk group only.
Why a transform-and-interpolate pattern? Floh's approval step treats multi-element approver lists as AND-of: every entry must reach a non-pending decision before the step advances (see the engine notes in
.cursor/rules/domain/floh-workflows.mdc). So you can't write[manager:{{requestor.id}}, group:helpdesk]and get an "either/or" — that would create both rows and require both decisions whenever a manager exists. Instead, we resolve the single correct approver reference inside a transform step and feed it into the approval step as one interpolated value.
Overview¶
flowchart TD
Submit[Portal Submit]
Start[Start]
Resolve["Resolve Target & Approver (transform)"]
Approve[Approval Gate]
SetPwd[Set Password connector]
NotifyOK[Notify Requestor success]
NotifyAudit[Notify Security audit]
NotifyReject[Notify Requestor rejected]
Done[End]
Submit --> Start --> Resolve --> Approve
Approve -->|approved| SetPwd
SetPwd --> NotifyOK
SetPwd --> NotifyAudit
NotifyOK --> Done
NotifyAudit --> Done
Approve -->|rejected| NotifyReject --> Done
The graph is identical to Workflow C — only the transform script and the approval step's Approvers field change.
Prerequisites specific to this variant¶
In addition to the shared prerequisites:
- A group named
google-password-reset-helpdesk(or similar) with at least one member. This group acts as the fallback approver when the requestor has no manager on file. - Some of your users should have their
manager_idpopulated in Floh. Without any manager links, this variant degrades to behaving like Workflow C with a single approver group, which works but is no different from Workflow C — in that case prefer Workflow C for clarity. - (Optional) A separate audit group as in Workflow C step 3.7.
How does Floh learn about managers? Today
manager_idis set directly on theuserrow (e.g. via admin UI, SQL migration, or a future connector sync). It is not auto-populated from OIDC claims yet. If your IdP emits a manager claim and you want it synced, that's tracked separately under issue #229 — Richer submitter attributes in interpolation.
Step 1 — Create the workflow¶
Same as Workflow C step 1, but use a distinct name (e.g. Google Workspace Password Reset (Manager-or-Helpdesk)) so you can publish both variants side-by-side and assign them to different submission groups.
Step 2 — Define variables¶
Identical to Workflow C step 2. The requestor Requestor preset now exposes requestor.manager automatically (a shallow { id, email, displayName } snapshot, or null when no manager is configured). No new variables required.
Step 3 — Add the workflow steps¶
3.1 — Start¶
Type: start. Entry point.
3.2 — Resolve Target & Approver¶
Type: transform. Replaces the "Resolve Target Email" transform from Workflow C — same targetEmail output, plus a new approverRef output that carries the routing decision.
| Config Field | Value |
|---|---|
| Script | See below |
| Outputs | targetEmail, approverRef |
Script:
const requestor = floh.variables.requestor;
const link = (requestor.externalIdentities || []).find(
(identity) => identity.connectorName === "google-workspace-prod",
);
if (!link) {
throw new Error("No linked Google Workspace identity found for this user. Contact IT.");
}
// Route the approval to the requestor's manager when one is configured;
// fall back to the helpdesk approver group otherwise. The approval step
// receives this as a single interpolated approver reference, so exactly
// one approval row is created per run.
const approverRef =
requestor.manager && requestor.manager.id
? "user:" + requestor.manager.id
: "group:google-password-reset-helpdesk";
floh.log.info("Password reset routing decision", {
hasManager: Boolean(requestor.manager),
approverRef,
});
return {
targetEmail: link.externalEmail || requestor.email,
approverRef,
};
Replace google-workspace-prod with the instance name of your Google Workspace connector and google-password-reset-helpdesk with the slug of your helpdesk group (the value the group lookup typeahead emits, not the display name).
The floh.log.info line records the routing decision in the run timeline so auditors can trace which path each request took without re-deriving it from the variable store.
3.3 — Approval Gate¶
Type: approval.
| Config Field | Value |
|---|---|
| Approvers | {{approverRef}} |
| Due Date | (optional) |
Transitions:
success→ Set Passworderror→ Notify — Rejected
The interpolated value resolves to either user:<manager-uuid> or group:google-password-reset-helpdesk at run time — exactly one approval row is created per run, so the AND-of multi-approver gotcha doesn't apply.
3.4 through 3.8¶
Identical to Workflow C steps 3.4 — 3.8. Set Password, the two notifications, the optional audit notification, and End all behave the same way.
For the audit notification body, you may want to add a line indicating which approver path was taken — pull approverRef into the body:
This makes it trivial for the security team to filter "manager-approved" vs "helpdesk-approved" requests in their inbox.
Step 4 — Wire the graph¶
Identical to Workflow C step 4. The first edge connects Start to Resolve Target & Approver instead of "Resolve Target Email" — the new step name is the only difference.
Step 5 — Save, test, publish¶
Same as Workflow C step 5, but test both branches:
- Submit as a user with a manager configured. Verify the approval task lands in the manager's inbox (not the helpdesk group's inbox), the manager can approve, and the password is applied.
- Submit as a user without a manager configured. Verify the task lands in the helpdesk group's inbox instead, that any helpdesk member can approve, and the password is applied.
- Verify the run timeline shows the
Password reset routing decisionlog line with the correcthasManagervalue for each test.
If the helpdesk-fallback path doesn't trigger when expected, double-check that requestor.manager is actually null for the test user — log into the admin user-detail page and confirm no manager is set, or run the workflow with debugLogging: true and inspect the variable store at run start.
Workflow E — Interactive Password Reset (Approve First, Then Prompt + Retry)¶
Heads up on Workflows A/B/C/C2 vs. Workflow E: Today the request catalog deliberately strips
secret: truevariables from the entries it serves to portal users (seeWorkflowRepository.findCatalogEntries), so a portal-submitted catalog request cannot collect the new password on the submission form even though those workflows declare asecret: truenewPasswordvariable. Until catalog-side secret input is supported, Workflows A/B/C/C2 are usable from non-catalog starts (e.g. an internal admin trigger that pre-populatesnewPassword), and Workflow E is the recommended pattern for any portal-submittable password reset. The interactive prompt below is exactly what unblocks portal users.
Even when catalog-side secret input lands, the upfront-collection model has structural drawbacks: if the connector ultimately rejects the value (Google policy mismatch, account locked, transient API error) the user has to start over — open a new request, type the password again, wait for re-approval — and they enter the password before they even know whether the request will be approved.
This variant flips the order:
- The catalog form collects only the intent to reset (and an optional justification).
- An approver decides yes/no first.
- After approval, the user is prompted in the portal task inbox to choose the new password — using a
user_promptstep withresponseType: "password". - The connector tries to apply it. If Google rejects the value, the workflow loops the user back to the same prompt with the failure reason inlined, without failing the run or asking them to resubmit.
- After a configurable number of failed attempts (
maxAttempts), the workflow gives up gracefully and notifies the user.
This pattern is the recommended shape for any "act on caller-chosen sensitive value, then verify" flow — setPassword is the motivating example, but the same shape works for setting an SSH key, choosing a vanity username, supplying an MFA recovery code, etc.
Overview¶
flowchart TD
Start[Start]
Resolve[Resolve Target Email]
Approve[Approval Gate]
Prompt[Prompt for New Password]
SetPwd[Set Password connector]
NotifyOK[Notify Success]
NotifyExhausted[Notify Exhausted]
NotifyReject[Notify Rejected]
Done[End]
Start --> Resolve --> Approve
Approve -->|approved| Prompt
Prompt --> SetPwd
SetPwd -->|success| NotifyOK --> Done
SetPwd -->|error| Prompt
Prompt -->|exhausted| NotifyExhausted --> Done
Approve -->|rejected| NotifyReject --> Done
The dashed implicit detail: every connector failure transitions back to the prompt via on: "error". The engine populates {{lastStepError.message}} (and {{setPassword.error.message}} for stable per-step addressing) so the prompt can include the failure reason verbatim.
Variables¶
| Name | Type | Required | Secret | Default | Description |
|---|---|---|---|---|---|
requestor |
Requestor | yes | no | — | The submitting user (auto-filled from session, hidden in form) |
justification |
String |
no | no | — | Reason for the reset, shown to the approver |
newPassword |
String |
no | yes | — | Filled in after approval via the prompt step; never on the form |
newPassword is secret and not required on the catalog form — its value is supplied through the in-portal prompt rather than the catalog submission. The validator enforces the full binding contract: a user_prompt with responseType: "password" must point at a workflow variable declared with both secret: true and type: "string". Save / publish, the in-editor designer check, the executor's pre-pause check, and the tasks/:id/respond endpoint all reject the workflow / submission if either constraint is violated, so a misconfigured workflow fails closed at the earliest possible point.
Steps¶
Resolve Target Email¶
Same transform step as Workflow A.
Approval Gate¶
Same as Workflow C, but the approver here decides before seeing the password (which the user hasn't supplied yet).
Prompt for New Password — user_prompt¶
| Config Field | Value |
|---|---|
| Assignee ID | {{requestor.id}} |
| Notify assignee | true (sends an email saying "your request was approved — open the portal to choose your password") |
| Prompt Message | Your password reset was approved. Choose a new password for {{targetEmail}}.{{#if lastStepError}}\n\nThe last attempt failed: {{lastStepError.message}}{{/if}} |
| Response Type | password |
| Output Variable | newPassword |
| Required | true |
| Max Attempts | 3 |
Transitions:
on: "success"→setPasswordon: "exhausted"→notifyExhausted(run gives up after the third failed attempt)
The prompt UI in the portal renders the value as a masked input with a show/hide toggle and shows an "Attempt 2 of 3" header on the second-and-later visit so the user knows the workflow looped them back.
Set Password — connector¶
Same connector config as Workflow A, except the transitions:
on: "success"→notifyOkon: "error"→prompt(loop back to the prompt step)
When the connector fails the engine writes a sanitized error envelope into the run variables:
{{setPassword.error.message}}— the failure message bound to this specific step (stable across loop iterations).{{lastStepError.message}}— the most-recent failure across any step (convenient one-liner).
The raw connector response stays on the task row (visible to admins via the run timeline) but is not promoted to workflow variables to avoid leaking connector-specific payloads through interpolation.
Notify Success / Notify Exhausted / Notify Rejected¶
Three terminal notification steps — success after the connector accepts the value, exhausted after maxAttempts failed prompts, and rejected for the unhappy approval path. None of them interpolate {{newPassword}}.
When to use this pattern vs. Workflow A/C¶
Pick this variant when any of these is true:
- The user shouldn't have to re-fill out the form to retry on a transient/recoverable failure.
- You want approval to happen on the intent rather than on the value (e.g., the approver is the user's manager and shouldn't see the password they're choosing).
- You want a hard ceiling on attempts so a stuck workflow eventually closes itself rather than waiting forever.
Pick Workflow A/C when:
- The first try is overwhelmingly likely to succeed (no policy enforcement, well-known account).
- You want the run to fail-fast and produce a re-runnable error rather than retry interactively.
Security notes for this variant¶
- The prompt's
outputVariablemust point at a workflow variable declared with bothsecret: trueandtype: "string". The portal renders the field masked, the value is encrypted at rest, and thetasks/:id/respondendpoint refuses to record the response if either constraint is violated (or the variable is undeclared). - The submitted value is intentionally omitted from the prompt task's
resultpayload — onlyresponseTypeandoutputVariableare recorded — so an admin viewing the task row never sees the cleartext password the user typed. - The error envelope written into workflow variables when the connector fails is sanitized down to
{ message, code? }. Connector-specific payloads (raw HTTP bodies, internal IDs) stay on the task row only. {{newPassword}}should still never be interpolated into anotificationbody or a downstream step that logs its config.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
Set Password — password: must be between 8 and 100 characters |
The submitted password failed length validation before it reached Google. | Re-submit with a value 8-100 chars. The validation runs server-side before any API call. |
Google API error 400: Password does not meet policy requirements |
Google's password strength rules rejected the value (e.g. domain admin enforces a minimum entropy). | Pick a stronger password. Floh enforces only Google's minimum 8-char floor; tenant policy may require more. |
Resolve Target Email throws "No linked Google Workspace identity" |
The submitter has no user_external_identity row pointing at the Google connector. |
Link the identity manually (admin > user > External Identities), or run a connector sync with the Email match strategy enabled. |
| The new password works in Google but the user is asked to change it again | Either Google's tenant policy mandates change-on-next-login, or you set changePasswordAtNextLogin: true. |
Tenant policy overrides the connector flag — review Admin Console > Security > Password management. The connector setting only governs Google's per-call default. |
| (Workflow C) Approval task never appears in any inbox | The google-password-reset-approvers group has no members, or the approver group reference is mistyped (extra space, wrong group name). |
In Groups, confirm the group exists and has at least one member. In the approval step's Approvers field, click the lookup icon and pick the group from the dropdown rather than typing it. |
| (Workflow C) Audit notification never fires after approval | The graph is missing the second outbound edge from Set Password to Notify Security — Audit, or the audit step's recipientGroupRef is empty. |
In the Graph tab, confirm Set Password has two outbound edges (one to each notification). In the audit step config, re-pick the audit group via the lookup icon. |
| (Workflow C) Submitter sees the new password applied without approval | An older draft of the workflow (without the approval gate) is still published in the catalog under the same name. | In Workflows, confirm only the approved variant is Published. Unpublish or delete any earlier self-service draft that bypasses approval. |
| (Workflow C2) Approval task always lands in the helpdesk inbox even for users with managers | Either the requestor's manager_id is not actually populated in Floh, or the transform script's manager check evaluated falsy unexpectedly. |
Open the user's admin detail page and confirm the Manager field is set. If it is, enable debugLogging on the workflow, re-submit, and inspect the run timeline for the Password reset routing decision log line — hasManager: false confirms the requestor variable lacked manager data; hasManager: true with a helpdesk task means the script returned the wrong approverRef. |
(Workflow C2) Approval step fails with No approvals could be created — unresolved approver(s) |
The transform produced an approverRef value that the engine could not resolve to a real user or group — typically a typo in the helpdesk group slug or a stale manager UUID. |
Check the run's variable store for the literal approverRef value. If it's group:<slug>, confirm the group exists and the slug matches exactly. If it's user:<uuid>, confirm the user is active and not soft-deleted. |
(Workflow E) Workflow validation rejects the prompt with outputVariable ... must be declared with secret: true |
The newPassword workflow variable is missing or not flagged secret. |
In the workflow Variables tab, confirm newPassword exists, has type string, and Secret is checked. The validator runs at save and publish time. |
(Workflow E) Run fails immediately at the prompt with code: "exhausted" on the first visible retry |
The step has already accumulated prior user_prompt tasks in the same run (e.g. an earlier loop iteration), so the next entry is already past maxAttempts. The engine counts every prior task for the step in the same run, including completed ones. (Separately: maxAttempts: 0 — or any value outside 1..20 — is a design-time error caught by the in-editor designer check and the save/publish validator (with the message user_prompt maxAttempts must be an integer between 1 and 20); the executor also re-asserts the bound as a final fail-fast guard. In every case the symptom is a validation error or a failed step, never code: "exhausted".) |
Keep maxAttempts between 1 and 20, and inspect the run timeline for the prompt step's prior task history if you're hitting the ceiling sooner than expected. |
| (Workflow E) Prompt loops forever even after the user enters a valid password | The connector is succeeding but the success transition was wired back to the prompt by accident. |
In the Set Password step's transitions, confirm on: "success" points at notifyOk and only on: "error" points back at the prompt. |
| (Workflow E) Last failure reason isn't shown in the prompt body | The prompt template uses {{lastStepError}} (object) instead of {{lastStepError.message}}, or the failure transition skipped the connector and routed somewhere else first. |
Use {{lastStepError.message}} (or the per-step variant {{setPassword.error.message}}) in the prompt's Prompt Message. Confirm no intermediate step overwrote lastStepError between the connector and the re-prompt. |
What's next¶
- Command reference — full list of commands, parameters, and output variables (including
resetPasswordfor admin-driven random resets androtatePasswordfor offboarding) - Account request workflow — companion workflow for provisioning new accounts
- External identities — manage per-user connector identity links
- Creating workflows — build more workflows following the same patterns