Flow completion
When a flow finishes, you control what the user sees. Use ctx.complete() to display a completion screen with a summary, automatically close the flow, or allow the user to restart it.
Basic completion
The simplest completion just needs a title:
return ctx.complete({
title: "Order processed",
});Add a description for more context:
return ctx.complete({
title: "Order processed",
description: "The order has been submitted and the customer will receive a confirmation email.",
});Completion with content
Display a summary of what happened using any of the display elements:
return ctx.complete({
title: "Refund complete",
description: "The refund has been processed successfully.",
content: [
ctx.ui.display.keyValue({
data: [
{ key: "Order", value: order.reference },
{ key: "Refund amount", value: `£${refundAmount}` },
{ key: "Refund ID", value: refund.id },
],
}),
],
});You can use any display element in the content array:
return ctx.complete({
title: "Stock count complete",
content: [
ctx.ui.display.banner({
title: "3 discrepancies found",
description: "Review the items below that don't match expected quantities.",
mode: "warning",
}),
ctx.ui.display.table({
data: discrepancies.map(d => ({
SKU: d.sku,
Expected: d.expected,
Counted: d.counted,
Variance: d.counted - d.expected,
})),
}),
],
});Returning data
Use the data property to return structured data from the flow. This is useful when the flow is triggered programmatically and you need to pass results back:
return ctx.complete({
title: "Order created",
data: {
orderId: order.id,
orderReference: order.reference,
totalAmount: order.total,
},
});The data is available when querying the flow's status via the API.
Auto-close
For flows that should complete without showing a completion screen, use autoClose. The flow closes immediately and shows a brief notification with your title and description:
return ctx.complete({
title: "Item scanned",
description: "Added to inventory",
autoClose: true,
});When autoClose is true, you cannot include content since there's no completion screen to display it on.
This is useful for:
- Quick operational tasks that don't need a summary
- Flows that are part of a larger workflow
- Repetitive tasks where the user will immediately start another flow
Allow restart
Let users restart the flow from the completion screen. This is useful for repetitive tasks like processing multiple orders or scanning items.
Manual restart (default)
Show a button that lets the user restart:
return ctx.complete({
title: "Order processed",
allowRestart: {
mode: "manual",
buttonLabel: "Process another order",
},
});If you don't specify mode, it defaults to manual.
Automatic restart
Automatically restart the flow after completion. The user sees a brief notification instead of a completion screen:
return ctx.complete({
title: "Item received",
allowRestart: {
mode: "auto",
},
});Restart with inputs
If your flow has inputs defined in the schema, you can pre-fill them for the restart:
flow ProcessOrders {
inputs {
warehouseId ID
}
@permission(roles: [Operator])
}return ctx.complete({
title: "Order processed",
allowRestart: {
mode: "manual",
buttonLabel: "Next order",
inputs: {
warehouseId: inputs.warehouseId, // Keep the same warehouse
},
},
});If all inputs are optional, the inputs property is optional. If any inputs are required, you must provide them:
// Flow with required input - must provide inputs
return ctx.complete({
allowRestart: {
inputs: {
orderId: nextOrderId, // Required
},
},
});
// Flow with only optional inputs - inputs are optional
return ctx.complete({
allowRestart: true, // Can use boolean shorthand
});
// Flow with no inputs - can use boolean
return ctx.complete({
allowRestart: true,
});Stages
If your flow uses stages, you can assign the completion to a stage:
export default ProcessReturn({
stages: [
{ key: "verify", name: "Verify" },
{ key: "process", name: "Process" },
{ key: "complete", name: "Complete" },
],
}, async (ctx, inputs) => {
// ... flow steps ...
return ctx.complete({
stage: "complete",
title: "Return processed",
content: [
// ...
],
});
});Full-width layout
For completion screens with wide content like tables, use fullWidth:
return ctx.complete({
title: "Inventory audit complete",
fullWidth: true,
content: [
ctx.ui.display.table({
data: auditResults,
}),
],
});Complete example
Here's a complete flow showing different completion patterns:
import { ProcessRefund, models } from "@teamkeel/sdk";
export default ProcessRefund({
stages: [
{ key: "select", name: "Select Order" },
{ key: "confirm", name: "Confirm" },
{ key: "complete", name: "Complete" },
],
}, async (ctx, inputs) => {
// Select order stage
const { orderId } = await ctx.ui.page("select", {
stage: "select",
title: "Select Order to Refund",
content: [
ctx.ui.select.table("orderId", {
label: "Order",
data: await getRefundableOrders(),
}),
],
});
const order = await models.order.findOne({ id: orderId });
// Confirm stage
const { action } = await ctx.ui.page("confirm", {
stage: "confirm",
title: "Confirm Refund",
content: [
ctx.ui.display.keyValue({
data: [
{ key: "Order", value: order.reference },
{ key: "Customer", value: order.customerName },
{ key: "Amount", value: `£${order.total}` },
],
}),
],
actions: [
{ label: "Process Refund", value: "refund", mode: "primary" },
{ label: "Cancel", value: "cancel" },
],
});
if (action === "cancel") {
return ctx.complete({
title: "Refund cancelled",
autoClose: true,
});
}
// Process the refund
const refund = await ctx.step("process refund", async () => {
const stripe = require("stripe")(ctx.secrets.STRIPE_KEY);
return await stripe.refunds.create(
{ payment_intent: order.paymentIntentId },
{ idempotencyKey: `refund-${order.id}` }
);
});
await ctx.step("update order", async () => {
await models.order.update(
{ id: order.id },
{ status: "Refunded", refundId: refund.id }
);
});
// Completion with summary and restart option
return ctx.complete({
stage: "complete",
title: "Refund processed",
description: "The customer will receive their refund within 5-10 business days.",
content: [
ctx.ui.display.banner({
title: "Success",
description: `Refund of £${order.total} has been initiated.`,
mode: "success",
}),
ctx.ui.display.keyValue({
data: [
{ key: "Order", value: order.reference },
{ key: "Refund ID", value: refund.id },
{ key: "Amount", value: `£${order.total}` },
],
}),
],
data: {
orderId: order.id,
refundId: refund.id,
amount: order.total,
},
allowRestart: {
mode: "manual",
buttonLabel: "Process another refund",
},
});
});Options reference
| Option | Type | Description |
|---|---|---|
title | string | Heading displayed on the completion screen |
description | string | Subheading with additional context |
content | DisplayElement[] | Array of display elements to show |
data | any | Structured data returned from the flow |
stage | string | Stage key to mark as complete |
autoClose | boolean | Close immediately without showing completion screen |
fullWidth | boolean | Use full-width layout for wide content |
allowRestart | boolean | object | Enable restart functionality |
allowRestart.mode | "manual" | "auto" | Show button (manual) or restart automatically (auto) |
allowRestart.buttonLabel | string | Custom label for the restart button |
allowRestart.inputs | object | Input values to use when restarting |