# User input and MFA `ctx.input` pauses execution and lets Link display a form. Submitting resumes the same run. ## Request a code Inside a flow's `exec(ctx)`: ```ts const { code } = await ctx.input("verification", { purpose: "operation", schema: z.object({ code: z.string().regex(/^\d{6}$/) }), fields: { code: { label: "Verification code", type: "text" } }, message: "Enter the code sent by the website to confirm cancellation.", }); ``` Import `z` from `zod`. Use `code` in the next durable step to submit the actual website form. | Purpose | Example | App behavior | | ------------ | ------------------------- | ---------------------------------------- | | `connection` | Expired login needs MFA | Ask the account holder to restore access | | `operation` | Cancellation confirmation | Ask the user to confirm this operation | Authentication flows default to connection purpose. Other flows default to operation purpose. ![Hosted Link waits for a verification code from the account holder.](/docs/hosted-mfa.png) ## Rejected input Inside `exec(ctx)`, call your website's verification helper from a durable step: ```ts for (let attempt = 0; attempt < 3; attempt++) { const { code } = await ctx.input("verification", { purpose: "operation", schema: z.object({ code: z.string().min(1) }), fields: { code: { label: "Verification code", type: "text" } }, ...(attempt ? { feedback: { tone: "error" as const, message: "Incorrect code. Try again.", }, } : {}), }); const accepted = await ctx.step("verify", () => submitCode(ctx, code)); if (accepted) return { verified: true }; } throw new FlowError("VERIFICATION_REJECTED", "Verification attempts exhausted"); ``` `submitCode(ctx, code)` is your website helper: submit the real form and return whether the website accepted it. Import `FlowError` from the SDK and use a matching output schema for your flow. Each request is a new occurrence; a previous code is not reused. Supported field types are `text`, `email` and `password`; schemas describe flat string fields. ## Foreground and background | Run | What happens when it needs input | | -------------------- | ---------------------------------------------------- | | User-driven endpoint | Open hosted Link for the waiting run | | Scheduled sync | Send `run.requires_input`; let the user return later | | Sync already waiting | No overlapping scheduled occurrence is created | **Next:** [Resume a waiting run from your app](/docs/connect#resume-an-existing-run).