# Sequence builder

> Each sequence's email, wait and task steps in order, with its enrollment counts, an Add step dialog and an Activate or Pause button.

Source: https://bettersupabase.com/docs/registry/ui-recipes/sequence-builder

{/* Generated by scripts/build-recipes.ts from the example apps. Edit the examples or the script, then run `pnpm recipes`. */}

It loads its data on the server through the block's client; the [sequences block](https://bettersupabase.com/blocks/sequences) must be installed (`npx better-supabase add sequences`). Its page on bettersupabase.com is [/recipes/sequence-builder](https://bettersupabase.com/recipes/sequence-builder).

Web, with shadcn components:

```bash
npx shadcn add alert badge button card dialog empty field input select skeleton
```

```tsx title="apps/examples/nextjs/src/features/sequences/components/sequence-builder.tsx"
const statusVariants = {
  draft: "secondary",
  active: "success-light",
  paused: "warning-light",
  archived: "outline",
} as const satisfies Record<SequenceStatus, string>;

const stepIcons = {
  email: MailIcon,
  wait: ClockIcon,
  task: ListChecksIcon,
} as const satisfies Record<SequenceStep["kind"], LucideIcon>;

/** Render inside `<Suspense>`: the organization's sequences and their steps. */
export async function SequenceBuilder() {
  const [view, session, t] = await Promise.all([
    getSequences(),
    getSession(),
    getExtracted("sequences"),
  ]);

  if (view.kind === "no-organization") {
    return null;
  }

  if (view.kind === "error") {
    return (
      <Alert variant="destructive">
        <TriangleAlertIcon />
        <AlertTitle>{t("The sequences didn't load")}</AlertTitle>
        <AlertDescription>{t("Try again in a moment.")}</AlertDescription>
        <AlertAction>
          <form action={retry}>
            <Button size="sm" variant="outline" type="submit">
              {t("Retry")}
            </Button>
          </form>
        </AlertAction>
      </Alert>
    );
  }

  const manage = can(session, "sequences.manage");

  if (view.data.length === 0) {
    return (
      <Empty className="border">
        <EmptyHeader>
          <EmptyMedia variant="icon">
            <SendIcon />
          </EmptyMedia>
          <EmptyTitle>{t("No sequences yet")}</EmptyTitle>
          <EmptyDescription>
            {manage
              ? t(
                  "Create a sample sequence, then activate it to enroll contacts.",
                )
              : t(
                  "An admin sets up the sequences your team enrolls contacts in.",
                )}
          </EmptyDescription>
        </EmptyHeader>
        {manage ? (
          <EmptyContent>
            <SampleSequenceButton />
          </EmptyContent>
        ) : null}
      </Empty>
    );
  }

  const statusLabels: Record<SequenceStatus, string> = {
    draft: t("Draft"),
    active: t("Active"),
    paused: t("Paused"),
    archived: t("Archived"),
  };

  const stepLabel = (step: SequenceStep): string => {
    switch (step.kind) {
      case "email": {
        return step.subject;
      }

      case "wait": {
        return t("Wait {minutes} minutes", { minutes: String(step.minutes) });
      }

      case "task": {
        return step.title;
      }

      default: {
        const unknown: never = step;

        return unknown;
      }
    }
  };

  return (
    <div className="grid gap-4 lg:grid-cols-2">
      {view.data.map((sequence) => (
        <Card key={sequence.id}>
          <CardHeader>
            <div className="flex items-center gap-2">
              <CardTitle>{sequence.name}</CardTitle>
              <Badge variant={statusVariants[sequence.status]}>
                {statusLabels[sequence.status]}
              </Badge>
            </div>
            {manage && sequence.status !== "archived" ? (
              <CardAction className="flex gap-2">
                <AddStepDialog sequenceId={sequence.id} />
                <SequenceStatusButton
                  sequenceId={sequence.id}
                  active={sequence.status === "active"}
                />
              </CardAction>
            ) : null}
          </CardHeader>
          <CardContent>
            {sequence.steps.length === 0 ? (
              <p className="text-sm text-muted-foreground">
                {t("No steps yet.")}
              </p>
            ) : (
              <ol className="flex flex-col gap-2">
                {sequence.steps.map((step, index) => {
                  const Icon = stepIcons[step.kind];

                  return (
                    <li
                      key={step.id}
                      className="flex items-center gap-2 text-sm"
                    >
                      <span className="w-5 text-xs text-muted-foreground tabular-nums">
                        {index + 1}
                      </span>
                      <Icon
                        aria-hidden
                        className="size-4 text-muted-foreground"
                      />
                      <span className="truncate">{stepLabel(step)}</span>
                    </li>
                  );
                })}
              </ol>
            )}
          </CardContent>
          <CardFooter className="gap-4 text-xs text-muted-foreground tabular-nums">
            <span>
              {t("{count} active", { count: String(sequence.counts.active) })}
            </span>
            <span>
              {t("{count} completed", {
                count: String(sequence.counts.completed),
              })}
            </span>
            <span>
              {t("{count} replied", { count: String(sequence.counts.replied) })}
            </span>
          </CardFooter>
        </Card>
      ))}
    </div>
  );
}
```