Completion

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

OptionTypeDescription
titlestringHeading displayed on the completion screen
descriptionstringSubheading with additional context
contentDisplayElement[]Array of display elements to show
dataanyStructured data returned from the flow
stagestringStage key to mark as complete
autoClosebooleanClose immediately without showing completion screen
fullWidthbooleanUse full-width layout for wide content
allowRestartboolean | objectEnable restart functionality
allowRestart.mode"manual" | "auto"Show button (manual) or restart automatically (auto)
allowRestart.buttonLabelstringCustom label for the restart button
allowRestart.inputsobjectInput values to use when restarting