Skip to main content

User onboarding process

Guide a new user from account creation through email verification and two required setup tasks.

Product requirements

The process accepts a username, email address, first name, and last name. It must:

  1. Save the signup form, mark the user as waiting for verification, and send a verification email.
  2. Wait for the user to verify the email. Send a reminder every 24 seconds in the example until verification arrives.
  3. Ask the verified user to accomplish task 1, then wait for its completion. Send task 1 reminders while it remains incomplete.
  4. Ask the user to accomplish task 2 only after task 1 completes. Send task 2 reminders while it remains incomplete.
  5. Mark onboarding as completed, send a welcome email, and complete the Flow only after task 2 finishes.
  6. Expose verification and both task completions as RPCs so an application can advance the process while it is waiting.

Each waiting Step uses AnyOf with a Timer and a Channel. A Channel message advances the user immediately. A Timer sends a reminder and moves the same Step back to itself. A Worker can restart during any wait without losing the current stage, the reminder schedule, or a user action.

Potential failures and exceptional outcomes include:

  • A duplicate username cannot start a second Flow with the same Flow ID. The start endpoint reports that onboarding already exists.
  • Verification or a task may never arrive. The Flow remains durably waiting and sends reminders until it is completed, cancelled, or reaches its configured Flow timeout.
  • A task completion can arrive out of order. Its RPC returns that the task is not waiting and does not publish the Channel message.
  • A Step method or RPC can fail before it returns. Dex commits none of that method's Attribute writes or Channel publications, so a retry starts from the previously committed durable state.
  • Email delivery can fail transiently. The email adapter should surface the error so Dex can retry the Step.
  • An email provider can accept a message before the Worker records success. Production email calls need an idempotency key or equivalent deduplication.

The Go, Java, Python, TypeScript, and Rust examples all implement and integration-test the complete verification, task 1, task 2, and completion path.

Flow design

This interactive definition graph is generated from the runnable Python Flow.

Definition graph

UserOnboardingFlow

Valid

python · examples/python/dex_examples/products/signup/user_signup_flow.py

The Flow is a sequential state machine with interruptible waits:

SubmitVerifyEmailAccomplishTask1AccomplishTask2

It combines these design patterns:

  • Durable reminder moves a waiting Step back to itself whenever its Timer fires.
  • Interruptible wait uses AnyOf so a Channel message can wake the Step before the reminder Timer.
  • Responsive update uses RPCs to validate the current stage and publish the matching Channel message while the Flow is waiting.
  • Sequential state machine makes email verification and both tasks explicitly ordered. A later task cannot bypass an earlier one.
  • Durable progress state records each target stage in its WaitFor method. The Status Attribute changes when that durable wait is installed, so RPCs and operators observe the stage the Flow is actually waiting on.
  • Atomic method commit makes each WaitFor, Execute, and RPC invocation a separate commit boundary. Dex Server commits all Attribute writes and Channel publications from one method together only after it succeeds, so failed methods cannot expose partial durable state or messages.
  • Idempotent side effects let a retried Step safely resend an externally deduplicated email.

Separate Channels represent verification, task 1, and task 2. This keeps every external action scoped to exactly one wait and makes invalid transitions easy to reject.

Core implementation

Each tab starts with the Flow signature, Step topology, and persistence schema. The second snippet shows a representative Timer-or-Channel wait. The linked example includes every RPC, its HTTP controller, and an end-to-end integration test.

class UserOnboardingFlow(Flow[SignupForm]):
form = Attribute("Form", SignupForm)
status = Attribute("Status", str)
verify_email = Channel[None]("VerifyEmail", type(None))
task_1_completed = Channel[None]("Task1Completed", type(None))
task_2_completed = Channel[None]("Task2Completed", type(None))

def __init__(self, service: MyDependencyService) -> None:
self.service = service
self.verify_step = VerifyEmail(service, self.form, self.verify_email, self.status)
self.task_1_step = AccomplishTask1(
service, self.form, self.status, self.task_1_completed
)
self.task_2_step = AccomplishTask2(
service, self.form, self.status, self.task_2_completed
)
self.submit = Submit(service, self.form, self.status, self.verify_step)

def get_steps(self) -> StepList[SignupForm]:
return StepList.start_step(self.submit).other_steps(
self.verify_step,
self.task_1_step,
self.task_2_step,
)

def get_persistence_schema(self) -> PersistenceSchema:
return PersistenceSchema.of(
self.form,
self.status,
self.verify_email,
self.task_1_completed,
self.task_2_completed,
)
class AccomplishTask1(Step[None]):
def wait_for(self, context: Context, input: None) -> Wait:
self.status.set(context, "waiting_for_task_1")
return Wait.any_of(
Timer.by_duration(timedelta(seconds=24)),
self.task_1_completed.for_one(),
)

def execute(self, context: Context, input: None) -> StepDecision:
signup_form = self.form.get(context)
if self.task_1_completed.results(context):
self.service.send_email(
signup_form.email,
"complete onboarding task 2",
"task 2 is ready",
)
return go_to(AccomplishTask2, None)
self.service.send_email(
signup_form.email,
"task 1 reminder",
"please complete onboarding task 1",
)
return go_to(AccomplishTask1, None)

class UserOnboardingFlow(Flow[SignupForm]):
@rpc
def accomplish_task_1(self, context: Context) -> RPCResult[str]:
if self.status.get(context) != "waiting_for_task_1":
return RPCResult("task 1 is not waiting")
self.task_1_completed.publish(context, None)
return RPCResult("task 1 accomplished")

Example: examples/python/dex_examples/products/signup/user_signup_flow.py

Demo

This completed Go run submitted one user, verified the email, accomplished task 1, and accomplished task 2. The graph records each business stage as its own completed Step. Each waiting Step shows the Timer and Channel that were active before the user action arrived.

Completed UserOnboardingFlow Step execution graph

Open the lossless PNG to inspect the full-resolution graph.