Assign a dialog to a role
A dialog pauses the workflow until a person answers. In this recipe you let only the holders of a role answer it, and you test that everyone else is refused.
Save the two files as dialog-assignee.workflow.ts and
dialog-assignee.workflow.case.ts.
The host deploys only a workflow file’s default export, so one file holds one
workflow; the test imports the same definition by name.
import * as v from '@identity-flow/sdk/valibot';import { defineWorkflow } from '@identity-flow/sdk';
const Decision = v.object({ outcome: v.picklist(['approved', 'rejected']), reason: v.optional(v.string()),});
/** * `assignees` names a role at a provider, not a person. Who holds * `role:vacation-approver` is the provider's answer at the moment someone * answers, and it may change while the step is waiting; the step itself is * never rewritten. * * An account may answer when any one of its current principals matches any * assignee. Anyone else is refused: a refused answer writes nothing to the * instance, and the step stays pending. */export const vacationApproval = defineWorkflow( { name: 'vacation-approval', version: '1.0.0', draft: false, schema: v.object({ employeeId: v.string(), days: v.number() }), }, async (flow) => { const decision = await flow.dialog('approve vacation', { schema: Decision }, () => ({ params: { employeeId: flow.params.employeeId, days: flow.params.days }, assignees: [{ providerId: 'acme', subject: 'role:vacation-approver' }], }));
return { employeeId: flow.params.employeeId, outcome: decision.outcome }; },);
// The host deploys only the default export, so one file holds one workflow. The// test imports the named export; both are the same definition.export default vacationApproval;assignees lists principals:
{ providerId, subject } pairs that your identity provider reports for a
signed-in account, here the role role:vacation-approver at the provider
acme. An account may answer when one of its current principals matches one of
the assignees. IdentityFlow asks the provider for the account’s principals again
at the moment the account answers.
Name a role, not a person. When someone joins or leaves the role in your directory, the set of people who can answer changes without a change to the workflow.
params is what the person sees when they answer; the test reads it back as
dialog.data. Keep it to what they need for the decision: it is written to the
event history and stays there.
The schema in the options types the answer. decision has the type
{ outcome: 'approved' | 'rejected'; reason?: string }. In 0.3.0 an answer
that does not match the schema is not refused when it is submitted: the step and
then the instance end as ERRORED. If you build your own UI, check the answer
against the same rules before you submit it.
import { WorkflowTestError, workflowTest } from '@identity-flow/testing';import { expect } from 'vitest';
import { vacationApproval } from './dialog-assignee.workflow';
const test = workflowTest({ workflow: vacationApproval, accounts: { requester: { providerId: 'acme', principals: ['user:e-2041'] }, approver: { providerId: 'acme', principals: ['role:vacation-approver'] }, },});
test('lets only the assigned role answer', async ({ flow, accounts }) => { const instance = await flow.actAs(accounts.requester).start({ employeeId: 'e-2041', days: 5 }); const dialog = await flow.waitFor(instance).toHaveDialog('approve vacation');
expect(dialog.assignees).toEqual([{ providerId: 'acme', subject: 'role:vacation-approver' }]); expect(dialog.data).toEqual({ employeeId: 'e-2041', days: 5 });
await expect( flow.actAs(accounts.requester).completeActivity(dialog, { outcome: 'approved' }), ).rejects.toBeInstanceOf(WorkflowTestError);
await flow.actAs(accounts.approver).completeActivity(dialog, { outcome: 'approved' }); const completed = await flow.waitFor(instance).toBeCompleted();
expect(completed.data).toEqual({ employeeId: 'e-2041', outcome: 'approved' });
const observed = await flow.observe(instance); expect(observed.steps).toContainEqual( expect.objectContaining({ name: 'approve vacation', status: 'COMPLETED' }), );});accounts declares test accounts with fixed principals, and flow.actAs picks
the account that acts. The requester holds only user:e-2041, so their answer
is refused with a WorkflowTestError. A refused answer writes nothing to the
instance: the dialog stays pending, and the approver can still answer it.
Run the test with the Workflow Test distribution, as described in Before you start.
Next: Withdraw a dialog after a deadline for a dialog nobody answers, and Test who may act for how eligibility is decided in a Workflow Test.