# Workflow Runs

> Run history with a replayable timeline and log.

A shadcn/ui block from the [@notra](https://ui.usenotra.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/notra/workflow-runs).

- Page, with a live preview: https://shadcn.exchange/r/notra/workflow-runs
- Screenshot: https://shadcn.exchange/shot/notra/workflow-runs/light.png
- Registry item: https://ui.usenotra.com/r/workflow-runs.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/notra/workflow-runs.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@base-ui/react`, `@xyflow/react`, `class-variance-authority`, `cn`, `lucide-react`
- Registry items: `badge`

## Files

### `registry/notra/workflow/components/workflow-block-icon.tsx`

```tsx
import { cn } from "cn";

import { WORKFLOW_KINDS } from "../constants/workflow";
import { findModel } from "../lib/workflow";
import type { WorkflowBlockIconProps } from "../types/workflow";
import { WORKFLOW_LOGOS, WorkflowLogo } from "./workflow-logo";

export const WorkflowBlockIcon = ({
  block,
  className,
}: WorkflowBlockIconProps) => {
  const logo = block.logo ?? findModel(block.model)?.logo;
  const kind = WORKFLOW_KINDS[block.kind];
  const KindIcon = kind.icon;

  if (logo && WORKFLOW_LOGOS[logo].tile) {
    return (
      <span
        className={cn(
          "flex size-7 shrink-0 overflow-hidden rounded-md",
          className
        )}
        data-slot="workflow-block-icon"
      >
        <WorkflowLogo className="size-full" name={logo} />
      </span>
    );
  }

  return (
    <span
      className={cn(
        "flex size-7 shrink-0 items-center justify-center rounded-md",
        logo
          ? "bg-background border-foreground/10 border"
          : cn("ring-1 ring-inset", kind.tileClassName),
        className
      )}
      data-slot="workflow-block-icon"
    >
      {logo ? (
        <WorkflowLogo className="size-[58%]" name={logo} />
      ) : (
        <KindIcon className="size-1/2" />
      )}
    </span>
  );
};
```

### `registry/notra/workflow/components/workflow-logo.tsx`

```tsx
import { cn } from "cn";

import type {
  WorkflowLogoConfig,
  WorkflowLogoName,
  WorkflowLogoProps,
} from "../types/workflow";
import {
  AhrefsLogo,
  AnthropicLogo,
  BrowserbaseLogo,
  ClaudeLogo,
  ContextDevLogo,
  DeepSeekLogo,
  FirecrawlLogo,
  GeminiLogo,
  GitHubLogo,
  GmailLogo,
  GoogleLogo,
  GoogleSheetsLogo,
  GrokLogo,
  LinearLogo,
  MistralLogo,
  NotionLogo,
  OpenAILogo,
  PerplexityLogo,
  PostHogLogo,
  ResendLogo,
  SlackLogo,
  WebflowLogo,
  WordPressLogo,
} from "./workflow-logos";

export const WORKFLOW_LOGOS: Record<WorkflowLogoName, WorkflowLogoConfig> = {
  ahrefs: { label: "Ahrefs", logo: AhrefsLogo },
  anthropic: { label: "Anthropic", logo: AnthropicLogo },
  browserbase: { label: "Browserbase", logo: BrowserbaseLogo, tile: true },
  claude: { label: "Claude", logo: ClaudeLogo },
  deepseek: { label: "DeepSeek", logo: DeepSeekLogo },
  "context-dev": { label: "context.dev", logo: ContextDevLogo, tile: true },
  firecrawl: { label: "Firecrawl", logo: FirecrawlLogo },
  gemini: { label: "Gemini", logo: GeminiLogo },
  github: { label: "GitHub", logo: GitHubLogo },
  gmail: { label: "Gmail", logo: GmailLogo },
  google: { label: "Google", logo: GoogleLogo },
  "google-sheets": { label: "Google Sheets", logo: GoogleSheetsLogo },
  grok: { label: "Grok", logo: GrokLogo },
  linear: { label: "Linear", logo: LinearLogo },
  mistral: { label: "Mistral", logo: MistralLogo },
  notion: { label: "Notion", logo: NotionLogo },
  openai: { label: "OpenAI", logo: OpenAILogo },
  perplexity: { label: "Perplexity", logo: PerplexityLogo },
  posthog: { label: "PostHog", logo: PostHogLogo },
  resend: { label: "Resend", logo: ResendLogo },
  slack: { label: "Slack", logo: SlackLogo },
  webflow: { label: "Webflow", logo: WebflowLogo },
  wordpress: { label: "WordPress", logo: WordPressLogo },
};

export const WorkflowLogo = ({
  className,
  name,
  ...props
}: WorkflowLogoProps) => {
  const Logo = WORKFLOW_LOGOS[name].logo;
  return (
    <Logo
      className={cn("size-4 shrink-0 overflow-visible", className)}
      data-slot="workflow-logo"
      {...props}
    />
  );
};
```

### `registry/notra/workflow/components/workflow-logos.tsx`

```tsx
import type { SVGProps } from "react";

export const OpenAILogo = (props: SVGProps<SVGSVGElement>) => (
  <svg
    viewBox="105.31 105.31 400.39 400.39"
    fill="currentColor"
    aria-hidden="true"
    {...props}
  >
    <path
      fillRule="evenodd"
      clipRule="evenodd"
      d="M252.794 108.802C289.191 99.0484 326.265 110.305 351.148 135.135C385.113 126.072 422.85 134.862 449.492 161.505C476.136 188.149 484.925 225.888 475.862 259.85V259.854C500.696 284.735 511.95 321.81 502.198 358.207C492.447 394.602 464.161 421.084 430.215 430.217C421.083 464.162 394.603 492.448 358.206 502.199C321.812 511.951 284.734 500.693 259.852 475.864C225.887 484.927 188.15 476.137 161.507 449.495C134.864 422.851 126.073 385.111 135.136 351.149C110.304 326.266 99.0496 289.192 108.801 252.795C118.552 216.4 146.84 189.918 180.784 180.785C189.917 146.841 216.396 118.553 252.794 108.802ZM374.292 407.145C374.292 411.271 372.092 415.086 368.517 417.148L283.723 466.102C302.487 480.585 327.555 486.459 352.217 479.852C386.997 470.532 410.068 439.312 410.555 405.006V317.717C410.555 315.08 409.125 312.621 406.843 311.303L374.292 292.509V407.145ZM251.868 415.897C248.296 417.959 243.893 417.959 240.317 415.897L155.526 366.942C152.366 390.436 159.811 415.08 177.866 433.136H177.863C203.325 458.594 241.896 462.962 271.85 446.232L347.449 402.586C349.735 401.268 351.148 398.8 351.148 396.163V358.579L251.868 415.897ZM368.602 220.628C366.319 219.309 363.474 219.318 361.191 220.637L328.641 239.431L427.921 296.749C431.496 298.811 433.697 302.627 433.697 306.752V404.661C455.622 395.654 473.244 376.881 479.851 352.218C489.169 317.442 473.668 281.85 444.201 264.274L368.602 220.628ZM177.303 206.34C155.377 215.348 137.756 234.122 131.148 258.783C121.832 293.561 137.331 329.153 166.799 346.727L242.398 390.373C244.68 391.692 247.525 391.684 249.807 390.366L282.357 371.572L183.078 314.253C179.504 312.189 177.303 308.375 177.303 304.251V206.34ZM259.849 279.145V331.858L305.5 358.213L351.15 331.858V279.145L305.5 252.789L259.849 279.145ZM327.276 144.9C308.512 130.418 283.445 124.543 258.782 131.15C224.002 140.471 200.931 171.691 200.445 205.995V293.286C200.445 295.923 201.875 298.381 204.158 299.7L236.707 318.493V203.856C236.707 199.731 238.909 195.916 242.483 193.853L327.276 144.9ZM433.137 177.867C407.675 152.407 369.103 148.038 339.149 164.769L263.55 208.415C261.265 209.734 259.852 212.202 259.852 214.838V252.423L359.132 195.105C362.703 193.041 367.108 193.041 370.682 195.105L455.473 244.06C458.635 220.567 451.189 195.922 433.135 177.867H433.137Z"
    />
  </svg>
);

export const AnthropicLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg
    viewBox="0 3.52 24 16.48"
    fill="currentColor"
    aria-hidden="true"
    {...props}
  >
    <path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z" />
  </svg>
);

export const ClaudeLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 256 257" aria-hidden="true" {...props}>
    <path
      fill="#D97757"
      d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"
    />
  </svg>
);

export const GeminiLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 296 298" fill="none" aria-hidden="true" {...props}>
    <mask
      id="gemini-a"
      width="296"
      height="298"
      x="0"
      y="0"
      maskUnits="userSpaceOnUse"
      style={{ maskType: "alpha" }}
    >
      <path
        fill="#3186FF"
        d="M141.201 4.886c2.282-6.17 11.042-6.071 13.184.148l5.985 17.37a184.004 184.004 0 0 0 111.257 113.049l19.304 6.997c6.143 2.227 6.156 10.91.02 13.155l-19.35 7.082a184.001 184.001 0 0 0-109.495 109.385l-7.573 20.629c-2.241 6.105-10.869 6.121-13.133.025l-7.908-21.296a184 184 0 0 0-109.02-108.658l-19.698-7.239c-6.102-2.243-6.118-10.867-.025-13.132l20.083-7.467A183.998 183.998 0 0 0 133.291 26.28l7.91-21.394Z"
      />
    </mask>
    <g mask="url(#gemini-a)">
      <g filter="url(#gemini-b)">
        <ellipse cx="163" cy="149" fill="#3689FF" rx="196" ry="159" />
      </g>
      <g filter="url(#gemini-c)">
        <ellipse cx="33.5" cy="142.5" fill="#F6C013" rx="68.5" ry="72.5" />
      </g>
      <g filter="url(#gemini-d)">
        <ellipse cx="19.5" cy="148.5" fill="#F6C013" rx="68.5" ry="72.5" />
      </g>
      <g filter="url(#gemini-e)">
        <path
          fill="#FA4340"
          d="M194 10.5C172 82.5 65.5 134.333 22.5 135L144-66l50 76.5Z"
        />
      </g>
      <g filter="url(#gemini-f)">
        <path
          fill="#FA4340"
          d="M190.5-12.5C168.5 59.5 62 111.333 19 112L140.5-89l50 76.5Z"
        />
      </g>
      <g filter="url(#gemini-g)">
        <path
          fill="#14BB69"
          d="M194.5 279.5C172.5 207.5 66 155.667 23 155l121.5 201 50-76.5Z"
        />
      </g>
      <g filter="url(#gemini-h)">
        <path
          fill="#14BB69"
          d="M196.5 320.5C174.5 248.5 68 196.667 25 196l121.5 201 50-76.5Z"
        />
      </g>
    </g>
    <defs>
      <filter
        id="gemini-b"
        width="464"
        height="390"
        x="-69"
        y="-46"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="18"
        />
      </filter>
      <filter
        id="gemini-c"
        width="265"
        height="273"
        x="-99"
        y="6"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="32"
        />
      </filter>
      <filter
        id="gemini-d"
        width="265"
        height="273"
        x="-113"
        y="12"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="32"
        />
      </filter>
      <filter
        id="gemini-e"
        width="299.5"
        height="329"
        x="-41.5"
        y="-130"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="32"
        />
      </filter>
      <filter
        id="gemini-f"
        width="299.5"
        height="329"
        x="-45"
        y="-153"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="32"
        />
      </filter>
      <filter
        id="gemini-g"
        width="299.5"
        height="329"
        x="-41"
        y="91"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="32"
        />
      </filter>
      <filter
        id="gemini-h"
        width="299.5"
        height="329"
        x="-39"
        y="132"
        colorInterpolationFilters="sRGB"
        filterUnits="userSpaceOnUse"
      >
        <feFlood floodOpacity="0" result="BackgroundImageFix" />
        <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
        <feGaussianBlur
          result="effect1_foregroundBlur_69_17998"
          stdDeviation="32"
        />
      </filter>
    </defs>
  </svg>
);

export const PerplexityLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="8.94 4 30.12 40" aria-hidden="true" {...props}>
    <path
      fill="none"
      stroke="#20808d"
      strokeLinecap="round"
      strokeLinejoin="round"
      d="M24 4.5v39M13.73 16.573v-9.99L24 16.573m0 14.5L13.73 41.417V27.01L24 16.573m0 0l10.27-9.99v9.99"
    />
    <path
      fill="none"
      stroke="#20808d"
      strokeLinecap="round"
      strokeLinejoin="round"
      d="M13.73 31.396H9.44V16.573h29.12v14.823h-4.29"
    />
    <path
      fill="none"
      stroke="#20808d"
      strokeLinecap="round"
      strokeLinejoin="round"
      d="M24 16.573L34.27 27.01v14.407L24 31.073"
    />
  </svg>
);

export const MistralLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 256 233" aria-hidden="true" {...props}>
    <path d="M186.18182 0h46.54545v46.54545h-46.54545z" />
    <path fill="#F7D046" d="M209.45454 0h46.54545v46.54545h-46.54545z" />
    <path d="M0 0h46.54545v46.54545H0zM0 46.54545h46.54545V93.0909H0zM0 93.09091h46.54545v46.54545H0zM0 139.63636h46.54545v46.54545H0zM0 186.18182h46.54545v46.54545H0z" />
    <path fill="#F7D046" d="M23.27273 0h46.54545v46.54545H23.27273z" />
    <path
      fill="#F2A73B"
      d="M209.45454 46.54545h46.54545V93.0909h-46.54545zM23.27273 46.54545h46.54545V93.0909H23.27273z"
    />
    <path d="M139.63636 46.54545h46.54545V93.0909h-46.54545z" />
    <path
      fill="#F2A73B"
      d="M162.90909 46.54545h46.54545V93.0909h-46.54545zM69.81818 46.54545h46.54545V93.0909H69.81818z"
    />
    <path
      fill="#EE792F"
      d="M116.36364 93.09091h46.54545v46.54545h-46.54545zM162.90909 93.09091h46.54545v46.54545h-46.54545zM69.81818 93.09091h46.54545v46.54545H69.81818z"
    />
    <path d="M93.09091 139.63636h46.54545v46.54545H93.09091z" />
    <path
      fill="#EB5829"
      d="M116.36364 139.63636h46.54545v46.54545h-46.54545z"
    />
    <path
      fill="#EE792F"
      d="M209.45454 93.09091h46.54545v46.54545h-46.54545zM23.27273 93.09091h46.54545v46.54545H23.27273z"
    />
    <path d="M186.18182 139.63636h46.54545v46.54545h-46.54545z" />
    <path
      fill="#EB5829"
      d="M209.45454 139.63636h46.54545v46.54545h-46.54545z"
    />
    <path d="M186.18182 186.18182h46.54545v46.54545h-46.54545z" />
    <path fill="#EB5829" d="M23.27273 139.63636h46.54545v46.54545H23.27273z" />
    <path
      fill="#EA3326"
      d="M209.45454 186.18182h46.54545v46.54545h-46.54545zM23.27273 186.18182h46.54545v46.54545H23.27273z"
    />
  </svg>
);

export const DeepSeekLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 3 24 17.66" aria-hidden="true" {...props}>
    <path
      fill="#4D6BFE"
      d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 0 1-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 0 0-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 0 1-.465.137 9.597 9.597 0 0 0-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 0 0 1.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 0 1 1.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 0 1 .415-.287.302.302 0 0 1 .2.288.306.306 0 0 1-.31.307.303.303 0 0 1-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 0 1-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 0 1 .016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 0 1-.254-.078.253.253 0 0 1-.114-.358c.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z"
    />
  </svg>
);

export const GrokLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg
    viewBox="0 0 1024 1024"
    fill="currentColor"
    aria-hidden="true"
    {...props}
  >
    <path d="M395.479 633.828L735.91 381.105C752.599 368.715 776.454 373.548 784.406 392.792C826.26 494.285 807.561 616.253 724.288 699.996C641.016 783.739 525.151 802.104 419.247 760.277L303.556 814.143C469.49 928.202 670.987 899.995 796.901 773.282C896.776 672.843 927.708 535.937 898.785 412.476L899.047 412.739C857.105 231.37 909.358 158.874 1016.4 10.6326C1018.93 7.11771 1021.47 3.60279 1024 0L883.144 141.651V141.212L395.392 633.916" />
    <path d="M325.226 695.251C206.128 580.84 226.662 403.776 328.285 301.668C403.431 226.097 526.549 195.254 634.026 240.596L749.454 186.994C728.657 171.88 702.007 155.623 671.424 144.2C533.19 86.9942 367.693 115.465 255.323 228.382C147.234 337.081 113.244 504.215 171.613 646.833C215.216 753.423 143.739 828.818 71.7385 904.916C46.2237 931.893 20.6216 958.87 0 987.429L325.139 695.339" />
  </svg>
);

export const BrowserbaseLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 200 200" fill="none" aria-hidden="true" {...props}>
    <rect x="30" y="36" width="134" height="124" fill="white" />
    <path d="M111.168 116.901H83.168V109.901H111.168V116.901Z" fill="#FF4500" />
    <path d="M111.168 86.208H83.168V79.208H111.168V86.208Z" fill="#FF4500" />
    <path
      fillRule="evenodd"
      clipRule="evenodd"
      d="M200 200H0V0H200V200ZM55.4453 147.815H128.678L145.259 131.234V111.891L131.441 98.0723L142.495 87.0186V69.0557L125.914 52.4756H55.4453V147.815Z"
      fill="#FF4500"
    />
  </svg>
);

export const ContextDevLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 180 180" fill="none" aria-hidden="true" {...props}>
    <rect width="180" height="180" fill="#2663EB" />
    <g transform="translate(22 45) scale(0.1658)" fill="#FFFFFF">
      <path
        fillRule="evenodd"
        d="M287.56,0H745.26A27.06,27.06,0,0,1,772.32,27.06V484.77A27.06,27.06,0,0,1,745.26,511.83H284.51A24.01,24.01,0,0,1,260.5,487.82V27.06A27.06,27.06,0,0,1,287.56,0ZM260.5,358.28a153.55,153.55,0,1,0,307.1,0a153.55,153.55,0,1,0,-307.1,0Z"
      />
      <circle cx="153.55" cy="377.64" r="153.55" />
    </g>
  </svg>
);

export const FirecrawlLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 50 72" fill="none" aria-hidden="true" {...props}>
    <path
      fill="#fa5d19"
      d="M41.715 23.193c-2.762.82-4.844 2.675-6.37 4.69-.327.432-1.01.107-.88-.423 2.92-12.007-.937-21.986-12.961-26.898a.803.803 0 0 0-1.085.937c5.47 21.961-17.537 20.109-14.63 45.005.05.427-.43.72-.78.47-1.09-.782-2.307-2.415-3.142-3.562a.502.502 0 0 0-.887.16c-.665 2.404-.98 4.67-.98 6.92 0 8.749 4.497 16.45 11.304 20.915.39.255.89-.11.758-.557a13.5 13.5 0 0 1-.563-3.697c0-.788.05-1.593.173-2.343.285-1.885.94-3.68 2.04-5.314 3.772-5.663 11.334-11.132 10.127-18.56-.078-.47.477-.78.827-.457 5.328 4.868 6.383 11.415 5.508 17.287-.075.51.564.782.887.382a11.6 11.6 0 0 1 2.892-2.587c.27-.168.63-.04.733.26.602 1.752 1.497 3.397 2.342 5.042a13.46 13.46 0 0 1 .905 9.982.502.502 0 0 0 .755.57C45.5 66.95 50 59.248 50 50.494c0-3.043-.532-6.025-1.54-8.82-2.112-5.862-7.472-10.264-6.117-17.904.065-.365-.273-.682-.628-.577"
    />
  </svg>
);

export const ResendLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg
    viewBox="450 450 900 900"
    fill="currentColor"
    aria-hidden="true"
    {...props}
  >
    <path d="M1000.46 450C1174.77 450 1278.43 553.669 1278.43 691.282C1278.43 828.896 1174.77 932.563 1000.46 932.563H912.382L1350 1350H1040.82L707.794 1033.48C683.944 1011.47 672.936 985.781 672.935 963.765C672.935 932.572 694.959 905.049 737.161 893.122L908.712 847.244C973.85 829.812 1018.81 779.353 1018.81 713.298C1018.8 632.567 952.745 585.78 871.095 585.78H450V450H1000.46Z" />
  </svg>
);

export const SlackLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 2447.6 2452.5" aria-hidden="true" {...props}>
    <g clipRule="evenodd" fillRule="evenodd">
      <path
        d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
        fill="#36c5f0"
      />
      <path
        d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
        fill="#2eb67d"
      />
      <path
        d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
        fill="#ecb22e"
      />
      <path
        d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
        fill="#e01e5a"
      />
    </g>
  </svg>
);

export const GmailLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 64.72 512 384.1" aria-hidden="true" {...props}>
    <g fill="none" fillRule="evenodd">
      <g fillRule="nonzero">
        <path
          fill="#4285f4"
          d="M34.91 448.818h81.454V251L0 163.727V413.91c0 19.287 15.622 34.91 34.91 34.91z"
        />
        <path
          fill="#34a853"
          d="M395.636 448.818h81.455c19.287 0 34.909-15.622 34.909-34.909V163.727L395.636 251z"
        />
        <path
          fill="#fbbc04"
          d="M395.636 99.727V251L512 163.727v-46.545c0-43.142-49.25-67.782-83.782-41.891z"
        />
      </g>
      <path
        fill="#ea4335"
        d="M116.364 251V99.727L256 204.455 395.636 99.727V251L256 355.727z"
      />
      <path
        fill="#c5221f"
        fillRule="nonzero"
        d="M0 117.182v46.545L116.364 251V99.727L83.782 75.291C49.25 49.4 0 74.04 0 117.18z"
      />
    </g>
  </svg>
);

export const NotionLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 256 268" aria-hidden="true" {...props}>
    <path
      fill="#FFF"
      d="M16.092 11.538 164.09.608c18.179-1.56 22.85-.508 34.28 7.801l47.243 33.282C253.406 47.414 256 48.975 256 55.207v182.527c0 11.439-4.155 18.205-18.696 19.24L65.44 267.378c-10.913.517-16.11-1.043-21.825-8.327L8.826 213.814C2.586 205.487 0 199.254 0 191.97V29.726c0-9.352 4.155-17.153 16.092-18.188Z"
    />
    <path d="M164.09.608 16.092 11.538C4.155 12.573 0 20.374 0 29.726v162.245c0 7.284 2.585 13.516 8.826 21.843l34.789 45.237c5.715 7.284 10.912 8.844 21.825 8.327l171.864-10.404c14.532-1.035 18.696-7.801 18.696-19.24V55.207c0-5.911-2.336-7.614-9.21-12.66l-1.185-.856L198.37 8.409C186.94.1 182.27-.952 164.09.608ZM69.327 52.22c-14.033.945-17.216 1.159-25.186-5.323L23.876 30.778c-2.06-2.086-1.026-4.69 4.163-5.207l142.274-10.395c11.947-1.043 18.17 3.12 22.842 6.758l24.401 17.68c1.043.525 3.638 3.637.517 3.637L71.146 52.095l-1.819.125Zm-16.36 183.954V81.222c0-6.767 2.077-9.887 8.3-10.413L230.02 60.93c5.724-.517 8.31 3.12 8.31 9.879v153.917c0 6.767-1.044 12.49-10.387 13.008l-161.487 9.361c-9.343.517-13.489-2.594-13.489-10.921ZM212.377 89.53c1.034 4.681 0 9.362-4.681 9.897l-7.783 1.542v114.404c-6.758 3.637-12.981 5.715-18.18 5.715-8.308 0-10.386-2.604-16.609-10.396l-50.898-80.079v77.476l16.1 3.646s0 9.362-12.989 9.362l-35.814 2.077c-1.043-2.086 0-7.284 3.63-8.318l9.351-2.595V109.823l-12.98-1.052c-1.044-4.68 1.55-11.439 8.826-11.965l38.426-2.585 52.958 81.113v-71.76l-13.498-1.552c-1.043-5.733 3.111-9.896 8.3-10.404l35.84-2.087Z" />
  </svg>
);

export const GoogleLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 268.152 273.883" aria-hidden="true" {...props}>
    <defs>
      <linearGradient id="google-a">
        <stop offset="0" stopColor="#0fbc5c" />
        <stop offset="1" stopColor="#0cba65" />
      </linearGradient>
      <linearGradient id="google-g">
        <stop offset=".231" stopColor="#0fbc5f" />
        <stop offset=".312" stopColor="#0fbc5f" />
        <stop offset=".366" stopColor="#0fbc5e" />
        <stop offset=".458" stopColor="#0fbc5d" />
        <stop offset=".54" stopColor="#12bc58" />
        <stop offset=".699" stopColor="#28bf3c" />
        <stop offset=".771" stopColor="#38c02b" />
        <stop offset=".861" stopColor="#52c218" />
        <stop offset=".915" stopColor="#67c30f" />
        <stop offset="1" stopColor="#86c504" />
      </linearGradient>
      <linearGradient id="google-h">
        <stop offset=".142" stopColor="#1abd4d" />
        <stop offset=".248" stopColor="#6ec30d" />
        <stop offset=".312" stopColor="#8ac502" />
        <stop offset=".366" stopColor="#a2c600" />
        <stop offset=".446" stopColor="#c8c903" />
        <stop offset=".54" stopColor="#ebcb03" />
        <stop offset=".616" stopColor="#f7cd07" />
        <stop offset=".699" stopColor="#fdcd04" />
        <stop offset=".771" stopColor="#fdce05" />
        <stop offset=".861" stopColor="#ffce0a" />
      </linearGradient>
      <linearGradient id="google-f">
        <stop offset=".316" stopColor="#ff4c3c" />
        <stop offset=".604" stopColor="#ff692c" />
        <stop offset=".727" stopColor="#ff7825" />
        <stop offset=".885" stopColor="#ff8d1b" />
        <stop offset="1" stopColor="#ff9f13" />
      </linearGradient>
      <linearGradient id="google-b">
        <stop offset=".231" stopColor="#ff4541" />
        <stop offset=".312" stopColor="#ff4540" />
        <stop offset=".458" stopColor="#ff4640" />
        <stop offset=".54" stopColor="#ff473f" />
        <stop offset=".699" stopColor="#ff5138" />
        <stop offset=".771" stopColor="#ff5b33" />
        <stop offset=".861" stopColor="#ff6c29" />
        <stop offset="1" stopColor="#ff8c18" />
      </linearGradient>
      <linearGradient id="google-d">
        <stop offset=".408" stopColor="#fb4e5a" />
        <stop offset="1" stopColor="#ff4540" />
      </linearGradient>
      <linearGradient id="google-c">
        <stop offset=".132" stopColor="#0cba65" />
        <stop offset=".21" stopColor="#0bb86d" />
        <stop offset=".297" stopColor="#09b479" />
        <stop offset=".396" stopColor="#08ad93" />
        <stop offset=".477" stopColor="#0aa6a9" />
        <stop offset=".568" stopColor="#0d9cc6" />
        <stop offset=".667" stopColor="#1893dd" />
        <stop offset=".769" stopColor="#258bf1" />
        <stop offset=".859" stopColor="#3086ff" />
      </linearGradient>
      <linearGradient id="google-e">
        <stop offset=".366" stopColor="#ff4e3a" />
        <stop offset=".458" stopColor="#ff8a1b" />
        <stop offset=".54" stopColor="#ffa312" />
        <stop offset=".616" stopColor="#ffb60c" />
        <stop offset=".771" stopColor="#ffcd0a" />
        <stop offset=".861" stopColor="#fecf0a" />
        <stop offset=".915" stopColor="#fecf08" />
        <stop offset="1" stopColor="#fdcd01" />
      </linearGradient>
      <linearGradient
        href="#google-a"
        id="google-s"
        x1="219.7"
        x2="254.467"
        y1="329.535"
        y2="329.535"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-b"
        id="google-m"
        cx="109.627"
        cy="135.862"
        r="71.46"
        fx="109.627"
        fy="135.862"
        gradientTransform="matrix(-1.93688 1.043 1.45573 2.55542 290.525 -400.634)"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-c"
        id="google-n"
        cx="45.259"
        cy="279.274"
        r="71.46"
        fx="45.259"
        fy="279.274"
        gradientTransform="matrix(-3.5126 -4.45809 -1.69255 1.26062 870.8 191.554)"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-d"
        id="google-l"
        cx="304.017"
        cy="118.009"
        r="47.854"
        fx="304.017"
        fy="118.009"
        gradientTransform="matrix(2.06435 0 0 2.59204 -297.679 -151.747)"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-e"
        id="google-o"
        cx="181.001"
        cy="177.201"
        r="71.46"
        fx="181.001"
        fy="177.201"
        gradientTransform="matrix(-.24858 2.08314 2.96249 .33417 -255.146 -331.164)"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-f"
        id="google-p"
        cx="207.673"
        cy="108.097"
        r="41.102"
        fx="207.673"
        fy="108.097"
        gradientTransform="matrix(-1.2492 1.34326 -3.89684 -3.4257 880.501 194.905)"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-g"
        id="google-r"
        cx="109.627"
        cy="135.862"
        r="71.46"
        fx="109.627"
        fy="135.862"
        gradientTransform="matrix(-1.93688 -1.043 1.45573 -2.55542 290.525 838.683)"
        gradientUnits="userSpaceOnUse"
      />
      <radialGradient
        href="#google-h"
        id="google-j"
        cx="154.87"
        cy="145.969"
        r="71.46"
        fx="154.87"
        fy="145.969"
        gradientTransform="matrix(-.0814 -1.93722 2.92674 -.11625 -215.135 632.86)"
        gradientUnits="userSpaceOnUse"
      />
      <filter
        id="google-q"
        width="1.097"
        height="1.116"
        x="-.048"
        y="-.058"
        colorInterpolationFilters="sRGB"
      >
        <feGaussianBlur stdDeviation="1.701" />
      </filter>
      <filter
        id="google-k"
        width="1.033"
        height="1.02"
        x="-.017"
        y="-.01"
        colorInterpolationFilters="sRGB"
      >
        <feGaussianBlur stdDeviation=".242" />
      </filter>
      <clipPath id="google-i" clipPathUnits="userSpaceOnUse">
        <path d="M371.378 193.24H237.083v53.438h77.167c-1.241 7.563-4.026 15.003-8.105 21.786-4.674 7.773-10.451 13.69-16.373 18.196-17.74 13.498-38.42 16.258-52.783 16.258-36.283 0-67.283-23.286-79.285-54.928-.484-1.149-.805-2.335-1.197-3.507a81.115 81.115 0 0 1-4.101-25.448c0-9.226 1.569-18.057 4.43-26.398 11.285-32.897 42.985-57.467 80.179-57.467 7.481 0 14.685.884 21.517 2.648a77.668 77.668 0 0 1 33.425 18.25l40.834-39.712c-24.839-22.616-57.219-36.32-95.844-36.32-30.878 0-59.386 9.553-82.748 25.7-18.945 13.093-34.483 30.625-44.97 50.985-9.753 18.879-15.094 39.8-15.094 62.294 0 22.495 5.35 43.633 15.103 62.337v.126c10.302 19.857 25.368 36.954 43.678 49.988 15.997 11.386 44.68 26.551 84.031 26.551 22.63 0 42.687-4.051 60.375-11.644 12.76-5.478 24.065-12.622 34.301-21.804 13.525-12.132 24.117-27.139 31.347-44.404 7.23-17.265 11.097-36.79 11.097-57.957 0-9.858-.998-19.87-2.689-28.968Z" />
      </clipPath>
    </defs>
    <g
      clipPath="url(#google-i)"
      transform="matrix(.95792 0 0 .98525 -90.174 -78.856)"
    >
      <path
        fill="url(#google-j)"
        d="M92.076 219.958c.148 22.14 6.501 44.983 16.117 63.424v.127c6.949 13.392 16.445 23.97 27.26 34.452l65.327-23.67c-12.36-6.235-14.246-10.055-23.105-17.026-9.054-9.066-15.802-19.473-20.004-31.677h-.17l.17-.127c-2.765-8.058-3.037-16.613-3.14-25.503Z"
        filter="url(#google-k)"
      />
      <path
        fill="url(#google-l)"
        d="M237.083 79.025c-6.456 22.526-3.988 44.421 0 57.161 7.457.006 14.64.888 21.45 2.647a77.662 77.662 0 0 1 33.424 18.25l41.88-40.726c-24.81-22.59-54.667-37.297-96.754-37.332Z"
        filter="url(#google-k)"
      />
      <path
        fill="url(#google-m)"
        d="M236.943 78.847c-31.67 0-60.91 9.798-84.871 26.359a145.533 145.533 0 0 0-24.332 21.15c-1.904 17.744 14.257 39.551 46.262 39.37 15.528-17.936 38.495-29.542 64.056-29.542l.07.002-1.044-57.335c-.048 0-.093-.004-.14-.004Z"
        filter="url(#google-k)"
      />
      <path
        fill="url(#google-n)"
        d="m341.475 226.379-28.268 19.285c-1.24 7.562-4.028 15.002-8.107 21.786-4.674 7.772-10.45 13.69-16.373 18.196-17.702 13.47-38.328 16.244-52.687 16.255-14.842 25.102-17.444 37.675 1.043 57.934 22.877-.016 43.157-4.117 61.046-11.796 12.931-5.551 24.388-12.792 34.761-22.097 13.706-12.295 24.442-27.503 31.769-45 7.327-17.497 11.245-37.282 11.245-58.734Z"
        filter="url(#google-k)"
      />
      <path
        fill="#3086ff"
        d="M234.996 191.21v57.498h136.006c1.196-7.874 5.152-18.064 5.152-26.5 0-9.858-.996-21.899-2.687-30.998Z"
        filter="url(#google-k)"
      />
      <path
        fill="url(#google-o)"
        d="M128.39 124.327c-8.394 9.119-15.564 19.326-21.249 30.364-9.753 18.879-15.094 41.83-15.094 64.324 0 .317.026.627.029.944 4.32 8.224 59.666 6.649 62.456 0-.004-.31-.039-.613-.039-.924 0-9.226 1.57-16.026 4.43-24.367 3.53-10.289 9.056-19.763 16.123-27.926 1.602-2.031 5.875-6.397 7.121-9.016.475-.997-.862-1.557-.937-1.908-.083-.393-1.876-.077-2.277-.37-1.275-.929-3.8-1.414-5.334-1.845-3.277-.921-8.708-2.953-11.725-5.06-9.536-6.658-24.417-14.612-33.505-24.216Z"
        filter="url(#google-k)"
      />
      <path
        fill="url(#google-p)"
        d="M162.099 155.857c22.112 13.301 28.471-6.714 43.173-12.977l-25.574-52.664a144.74 144.74 0 0 0-26.543 14.504c-12.316 8.512-23.192 18.9-32.176 30.72Z"
        filter="url(#google-q)"
      />
      <path
        fill="url(#google-r)"
        d="M171.099 290.222c-29.683 10.641-34.33 11.023-37.062 29.29a144.806 144.806 0 0 0 16.792 13.984c15.996 11.386 46.766 26.551 86.118 26.551.046 0 .09-.004.137-.004v-59.157l-.094.002c-14.736 0-26.512-3.843-38.585-10.527-2.977-1.648-8.378 2.777-11.123.799-3.786-2.729-12.9 2.35-16.183-.938Z"
        filter="url(#google-k)"
      />
      <path
        fill="url(#google-s)"
        d="M219.7 299.023v59.996c5.506.64 11.236 1.028 17.247 1.028 6.026 0 11.855-.307 17.52-.872v-59.748a105.119 105.119 0 0 1-17.477 1.461c-5.932 0-11.7-.686-17.29-1.865Z"
        filter="url(#google-k)"
        opacity=".5"
      />
    </g>
  </svg>
);

export const GoogleSheetsLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 74 100" fill="none" aria-hidden="true" {...props}>
    <mask
      id="google-sheets-mask0_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask0_1-52)">
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L56.4365 16.8843L45.398 1.43036Z"
        fill="#0F9D58"
      />
    </g>
    <mask
      id="google-sheets-mask1_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask1_1-52)">
      <path
        d="M18.9054 48.8962V80.908H54.2288V48.8962H18.9054ZM34.3594 76.4926H23.3209V70.9733H34.3594V76.4926ZM34.3594 67.6617H23.3209V62.1424H34.3594V67.6617ZM34.3594 58.8309H23.3209V53.3116H34.3594V58.8309ZM49.8134 76.4926H38.7748V70.9733H49.8134V76.4926ZM49.8134 67.6617H38.7748V62.1424H49.8134V67.6617ZM49.8134 58.8309H38.7748V53.3116H49.8134V58.8309Z"
        fill="#F1F1F1"
      />
    </g>
    <mask
      id="google-sheets-mask2_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask2_1-52)">
      <path
        d="M47.3352 25.9856L71.8905 50.5354V27.9229L47.3352 25.9856Z"
        fill="url(#google-sheets-paint0_linear_1-52)"
      />
    </g>
    <mask
      id="google-sheets-mask3_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask3_1-52)">
      <path
        d="M45.398 1.43036V21.2998C45.398 24.959 48.3618 27.9229 52.0211 27.9229H71.8905L45.398 1.43036Z"
        fill="#87CEAC"
      />
    </g>
    <mask
      id="google-sheets-mask4_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask4_1-52)">
      <path
        d="M7.86688 1.43036C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V8.60542C1.24374 4.9627 4.22415 1.98229 7.86688 1.98229H45.398V1.43036H7.86688Z"
        fill="white"
        fillOpacity="0.2"
      />
    </g>
    <mask
      id="google-sheets-mask5_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask5_1-52)">
      <path
        d="M65.2674 98.0177H7.86688C4.22415 98.0177 1.24374 95.0373 1.24374 91.3946V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V91.3946C71.8905 95.0373 68.9101 98.0177 65.2674 98.0177Z"
        fill="#263238"
        fillOpacity="0.2"
      />
    </g>
    <mask
      id="google-sheets-mask6_1-52"
      style={{ maskType: "alpha" }}
      maskUnits="userSpaceOnUse"
      x="1"
      y="1"
      width="71"
      height="98"
    >
      <path
        d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
        fill="white"
      />
    </mask>
    <g mask="url(#google-sheets-mask6_1-52)">
      <path
        d="M52.0211 27.9229C48.3618 27.9229 45.398 24.959 45.398 21.2998V21.8517C45.398 25.511 48.3618 28.4748 52.0211 28.4748H71.8905V27.9229H52.0211Z"
        fill="#263238"
        fillOpacity="0.1"
      />
    </g>
    <path
      d="M45.398 1.43036H7.86688C4.22415 1.43036 1.24374 4.41077 1.24374 8.0535V91.9465C1.24374 95.5893 4.22415 98.5697 7.86688 98.5697H65.2674C68.9101 98.5697 71.8905 95.5893 71.8905 91.9465V27.9229L45.398 1.43036Z"
      fill="url(#google-sheets-paint1_radial_1-52)"
    />
    <defs>
      <linearGradient
        id="google-sheets-paint0_linear_1-52"
        x1="59.6142"
        y1="28.0935"
        x2="59.6142"
        y2="50.5388"
        gradientUnits="userSpaceOnUse"
      >
        <stop stopColor="#263238" stopOpacity="0.2" />
        <stop offset="1" stopColor="#263238" stopOpacity="0.02" />
      </linearGradient>
      <radialGradient
        id="google-sheets-paint1_radial_1-52"
        cx="0"
        cy="0"
        r="1"
        gradientUnits="userSpaceOnUse"
        gradientTransform="translate(3.48187 3.36121) scale(113.917)"
      >
        <stop stopColor="white" stopOpacity="0.1" />
        <stop offset="1" stopColor="white" stopOpacity="0" />
      </radialGradient>
    </defs>
  </svg>
);

export const WebflowLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 1080 674" fill="none" aria-hidden="true" {...props}>
    <path
      fill="#146EF5"
      fillRule="evenodd"
      d="M1080 0 735.386 673.684H411.695l144.221-279.203h-6.471C430.464 548.934 252.942 650.61-.0005 673.684v-275.34s161.8135-9.557 256.9385-109.568H-.0005V.0053H288.771V237.515l6.481-.026L413.254.0053h218.39V236.009l6.482-.01L760.555 0H1080Z"
      clipRule="evenodd"
    />
  </svg>
);

export const WordPressLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 122.52 122.523" aria-hidden="true" {...props}>
    <g fill="#21759b">
      <path d="m8.708 61.26c0 20.802 12.089 38.779 29.619 47.298l-25.069-68.686c-2.916 6.536-4.55 13.769-4.55 21.388z" />
      <path d="m96.74 58.608c0-6.495-2.333-10.993-4.334-14.494-2.664-4.329-5.161-7.995-5.161-12.324 0-4.831 3.664-9.328 8.825-9.328.233 0 .454.029.681.042-9.35-8.566-21.807-13.796-35.489-13.796-18.36 0-34.513 9.42-43.91 23.688 1.233.037 2.395.063 3.382.063 5.497 0 14.006-.667 14.006-.667 2.833-.167 3.167 3.994.337 4.329 0 0-2.847.335-6.015.501l19.138 56.925 11.501-34.493-8.188-22.434c-2.83-.166-5.511-.501-5.511-.501-2.832-.166-2.5-4.496.332-4.329 0 0 8.679.667 13.843.667 5.496 0 14.006-.667 14.006-.667 2.835-.167 3.168 3.994.337 4.329 0 0-2.853.335-6.015.501l18.992 56.494 5.242-17.517c2.272-7.269 4.001-12.49 4.001-16.989z" />
      <path d="m62.184 65.857-15.768 45.819c4.708 1.384 9.687 2.141 14.846 2.141 6.12 0 11.989-1.058 17.452-2.979-.141-.225-.269-.464-.374-.724z" />
      <path d="m107.376 36.046c.226 1.674.354 3.471.354 5.404 0 5.333-.996 11.328-3.996 18.824l-16.053 46.413c15.624-9.111 26.133-26.038 26.133-45.426.001-9.137-2.333-17.729-6.438-25.215z" />
      <path d="m61.262 0c-33.779 0-61.262 27.481-61.262 61.26 0 33.783 27.483 61.263 61.262 61.263 33.778 0 61.265-27.48 61.265-61.263-.001-33.779-27.487-61.26-61.265-61.26zm0 119.715c-32.23 0-58.453-26.223-58.453-58.455 0-32.23 26.222-58.451 58.453-58.451 32.229 0 58.45 26.221 58.45 58.451 0 32.232-26.221 58.455-58.45 58.455z" />
    </g>
  </svg>
);

export const LinearLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 100 100" fill="none" aria-hidden="true" {...props}>
    <path
      fill="#5E6AD2"
      d="M1.225 61.523c-.222-.949.908-1.546 1.597-.857l36.512 36.512c.69.69.092 1.82-.857 1.597-18.425-4.323-32.93-18.827-37.252-37.252ZM.002 46.889a.99.99 0 0 0 .29.76L52.35 99.71c.201.2.478.307.76.29 2.37-.149 4.695-.46 6.963-.927.765-.157 1.03-1.096.478-1.648L2.576 39.448c-.552-.551-1.491-.286-1.648.479a50.067 50.067 0 0 0-.926 6.962ZM4.21 29.705a.988.988 0 0 0 .208 1.1l64.776 64.776c.289.29.726.375 1.1.208a49.908 49.908 0 0 0 5.185-2.684.981.981 0 0 0 .183-1.54L8.436 24.336a.981.981 0 0 0-1.541.183 49.896 49.896 0 0 0-2.684 5.185Zm8.448-11.631a.986.986 0 0 1-.045-1.354C21.78 6.46 35.111 0 49.952 0 77.592 0 100 22.407 100 50.048c0 14.84-6.46 28.172-16.72 37.338a.986.986 0 0 1-1.354-.045L12.659 18.074Z"
    />
  </svg>
);

export const GitHubLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg
    viewBox="0 0 1024 1024"
    fill="currentColor"
    aria-hidden="true"
    {...props}
  >
    <path
      fillRule="evenodd"
      d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28s87.04 5.76 128 17.28c97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0"
      clipRule="evenodd"
    />
  </svg>
);

export const AhrefsLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="16 8 96 112" fill="none" aria-hidden="true" {...props}>
    <path
      d="m24 8v24h64v16l-26.104 0.456c-35.096 3.256-45.896 11.624-45.896 36.504v6.512c0 19.064 14.328 28.528 32 28.528 7.984 0 16.32-1.56 25.04-5.976l14.96-6.024v12h24v-112zm64 76-18.112 7.472c-6.296 3.488-12.832 5.344-17.92 5.344-6.288 0-11.968-1.136-11.968-8.816v-5.12c0.48-8.136 7.376-11.176 26.256-13.032l21.744-1.848z"
      fill="#f80"
    />
  </svg>
);

export const PostHogLogo = (props: SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 2.41 47.99 26.58" fill="none" aria-hidden="true" {...props}>
    <path
      fill="#1D4AFF"
      d="M10.891 17.206a1 1 0 0 1-1.788 0l-.882-1.763a1 1 0 0 1 0-.894l.882-1.763a1 1 0 0 1 1.788 0l.882 1.763a1 1 0 0 1 0 .894l-.882 1.763zm0 9.997a1 1 0 0 1-1.788 0L8.22 25.44a1 1 0 0 1 0-.894l.882-1.763a1 1 0 0 1 1.788 0l.882 1.763a1 1 0 0 1 0 .894l-.882 1.763z"
    />
    <path
      fill="#F9BD2B"
      d="M0 23.408c0-.89 1.077-1.337 1.707-.707l4.583 4.583c.63.63.184 1.708-.707 1.708H1a1 1 0 0 1-1-1v-4.584zm0-4.828a1 1 0 0 0 .293.708l9.411 9.41a1 1 0 0 0 .707.294h5.17c.89 0 1.337-1.077.707-1.707l-14.58-14.58C1.077 12.074 0 12.52 0 13.41v5.17zm0-9.997a1 1 0 0 0 .293.707L19.7 28.7a1 1 0 0 0 .707.293h5.17c.89 0 1.337-1.078.707-1.708L1.707 2.707C1.077 2.077 0 2.523 0 3.414v5.17zm9.997 0a1 1 0 0 0 .293.707l17.994 17.995c.63.63 1.707.183 1.707-.708v-5.169a1 1 0 0 0-.293-.707L11.704 2.707c-.63-.63-1.707-.184-1.707.707v5.17zm11.704-5.876c-.63-.63-1.707-.184-1.707.707v5.17a1 1 0 0 0 .293.706l7.997 7.998c.63.63 1.707.183 1.707-.708v-5.169a1 1 0 0 0-.293-.707l-7.997-7.997z"
    />
    <path
      fill="#000"
      d="m42.525 23.53-9.413-9.412c-.63-.63-1.707-.184-1.707.707v13.167a1 1 0 0 0 1 1h14.58a1 1 0 0 0 1-1v-1.2c0-.552-.449-.993-.997-1.064a7.723 7.723 0 0 1-4.463-2.197zm-6.321 2.263a1.6 1.6 0 1 1 0-3.2 1.6 1.6 0 0 1 0 3.2z"
    />
    <path
      fill="#1D4AFF"
      d="M0 27.992a1 1 0 0 0 1 1h4.583c.891 0 1.337-1.078.707-1.708l-4.583-4.583c-.63-.63-1.707-.184-1.707.707v4.584zm9.997-16.995-8.29-8.29C1.077 2.077 0 2.523 0 3.414v5.17a1 1 0 0 0 .293.706l9.704 9.705v-7.998zm-8.29 1.707c-.63-.63-1.707-.184-1.707.707v5.17a1 1 0 0 0 .293.706l9.704 9.705v-7.998l-8.29-8.29z"
    />
    <path
      fill="#F54E00"
      d="M19.994 11.411a1 1 0 0 0-.293-.707l-7.997-7.997c-.63-.63-1.707-.184-1.707.707v5.17a1 1 0 0 0 .293.706l9.704 9.705V11.41zm-9.997 17.58h5.583c.891 0 1.337-1.077.707-1.707l-6.29-6.29v7.998zm0-17.994v7.583a1 1 0 0 0 .293.708l9.704 9.704v-7.584a1 1 0 0 0-.293-.707l-9.704-9.704z"
    />
  </svg>
);
```

### `registry/notra/workflow/components/workflow-run-button.tsx`

```tsx
"use client";

import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva } from "class-variance-authority";
import { cn } from "cn";
import { PlayIcon, RotateCcwIcon, SquareIcon } from "lucide-react";

import {
  WORKFLOW_RUN_LABELS,
  WORKFLOW_RUN_RING_LENGTH,
  WORKFLOW_RUN_RING_RADIUS,
} from "../constants/workflow-ui";
import type { WorkflowRunButtonProps } from "../types/workflow";

const runButtonVariants = cva(
  "group/run focus-visible:ring-ring/50 relative inline-flex h-8 shrink-0 items-center gap-2 rounded-full pr-3.5 pl-1 text-[0.8125rem] font-medium whitespace-nowrap transition-[background-color,box-shadow,color] duration-200 outline-none select-none focus-visible:ring-3 active:translate-y-px disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      phase: {
        done: "bg-foreground text-background hover:bg-foreground/90 shadow-[inset_0_1px_0_rgba(255,255,255,0.18),0_1px_2px_rgba(0,0,0,0.2)]",
        idle: "bg-foreground text-background hover:bg-foreground/90 shadow-[inset_0_1px_0_rgba(255,255,255,0.18),0_1px_2px_rgba(0,0,0,0.2)]",
        running:
          "bg-background text-foreground ring-foreground/15 hover:bg-muted ring-1",
      },
    },
  }
);

export const WorkflowRunButton = ({
  className,
  onClick,
  onPlay,
  onStop,
  phase,
  progress = 0,
  ...props
}: WorkflowRunButtonProps) => {
  const running = phase === "running";
  const ratio = Number.isFinite(progress)
    ? Math.min(1, Math.max(0, progress))
    : 0;
  let Icon = PlayIcon;
  if (running) {
    Icon = SquareIcon;
  } else if (phase === "done") {
    Icon = RotateCcwIcon;
  }

  return (
    <ButtonPrimitive
      className={cn(runButtonVariants({ phase }), className)}
      data-phase={phase}
      data-slot="workflow-run-button"
      {...props}
      onClick={(event) => {
        onClick?.(event);
        if (!event.defaultPrevented) {
          (running ? onStop : onPlay)?.();
        }
      }}
    >
      <span
        className={cn(
          "relative flex size-6 items-center justify-center rounded-full",
          running ? "text-primary" : "bg-background/15"
        )}
      >
        {running && (
          <svg
            aria-hidden="true"
            className="absolute inset-0 size-6 -rotate-90"
            viewBox="0 0 24 24"
          >
            <circle
              className="stroke-primary/20"
              cx={12}
              cy={12}
              fill="none"
              r={WORKFLOW_RUN_RING_RADIUS}
              strokeWidth={2}
            />
            <circle
              className="stroke-primary transition-[stroke-dashoffset] duration-100"
              cx={12}
              cy={12}
              fill="none"
              r={WORKFLOW_RUN_RING_RADIUS}
              strokeDasharray={WORKFLOW_RUN_RING_LENGTH}
              strokeDashoffset={WORKFLOW_RUN_RING_LENGTH * (1 - ratio)}
              strokeLinecap="round"
              strokeWidth={2}
            />
          </svg>
        )}
        <Icon
          className={cn(
            "size-3",
            running ? "fill-current" : "fill-current group-hover/run:scale-110",
            phase === "done" && "fill-none",
            "transition-transform"
          )}
        />
      </span>
      {WORKFLOW_RUN_LABELS[phase]}
    </ButtonPrimitive>
  );
};
```

### `registry/notra/workflow/components/workflow-run-history.tsx`

```tsx
import { cn } from "cn";

import { formatDuration } from "../lib/workflow";
import type { WorkflowRunHistoryProps } from "../types/workflow";
import { WorkflowStatusIndicator } from "./workflow-status";

export const WorkflowRunHistory = ({
  className,
  onSelect,
  runs,
  selectedId,
  ...props
}: WorkflowRunHistoryProps) => (
  <div
    className={cn("flex flex-col gap-0.5", className)}
    data-slot="workflow-run-history"
    {...props}
  >
    {runs.map((run) => (
      <button
        aria-current={run.id === selectedId || undefined}
        className="hover:bg-muted aria-current:bg-muted focus-visible:ring-ring/50 grid grid-cols-[1rem_minmax(0,1fr)_auto] items-center gap-2.5 rounded-lg px-2.5 py-2 text-left outline-none focus-visible:ring-2"
        data-slot="workflow-run-history-item"
        key={run.id}
        onClick={() => onSelect?.(run.id)}
        type="button"
      >
        <WorkflowStatusIndicator status={run.status} />
        <span className="flex min-w-0 flex-col">
          <span className="truncate text-[0.8125rem] leading-5 font-medium">
            {run.id}
          </span>
          <span className="text-muted-foreground truncate text-[0.6875rem] leading-4">
            {run.trigger} · {run.startedAt}
          </span>
        </span>
        <span className="flex flex-col items-end">
          <span className="font-mono text-[0.6875rem] tabular-nums">
            {formatDuration(run.durationMs)}
          </span>
          <span className="text-muted-foreground text-[0.6875rem]">
            {run.steps} steps
          </span>
        </span>
      </button>
    ))}
  </div>
);
```

### `registry/notra/workflow/components/workflow-run-log.tsx`

```tsx
import { cn } from "cn";

import { formatClock } from "../lib/workflow";
import type { WorkflowLogLevel, WorkflowRunLogProps } from "../types/workflow";

const LEVEL_CLASS: Record<WorkflowLogLevel, string> = {
  error: "bg-red-500",
  info: "bg-primary",
  skip: "bg-muted-foreground/40",
  success: "bg-emerald-500",
};

export const WorkflowRunLog = ({
  className,
  emptyLabel = "Run the workflow to see its log.",
  entries,
  ...props
}: WorkflowRunLogProps) => (
  <div
    className={cn(
      "flex flex-col font-mono text-[0.75rem] leading-5",
      className
    )}
    data-slot="workflow-run-log"
    role="log"
    {...props}
  >
    {entries.length === 0 ? (
      <p className="text-muted-foreground px-3 py-4 font-sans text-xs">
        {emptyLabel}
      </p>
    ) : (
      entries.map((entry) => (
        <div
          className="hover:bg-muted/60 motion-safe:animate-in motion-safe:fade-in-0 grid grid-cols-[4.5rem_0.5rem_minmax(0,1fr)] items-baseline gap-2.5 px-3 py-0.5"
          data-level={entry.level}
          data-slot="workflow-run-log-entry"
          key={entry.id}
        >
          <span className="text-muted-foreground tabular-nums">
            {formatClock(entry.at)}
          </span>
          <span
            aria-hidden="true"
            className={cn(
              "size-1.5 self-center rounded-full",
              LEVEL_CLASS[entry.level]
            )}
          />
          <span className="min-w-0 break-words">
            <span className="text-foreground font-medium">{entry.title}</span>
            <span
              className={cn(
                "text-muted-foreground",
                entry.level === "error" && "text-red-600 dark:text-red-400"
              )}
            >
              {" "}
              {entry.message}
            </span>
          </span>
        </div>
      ))
    )}
  </div>
);
```

### `registry/notra/workflow/components/workflow-run-timeline.tsx`

```tsx
import { cn } from "cn";

import { formatDuration, getNodeState } from "../lib/workflow";
import type { WorkflowRunTimelineProps } from "../types/workflow";
import { WorkflowBlockIcon } from "./workflow-block-icon";

export const WorkflowRunTimeline = ({
  className,
  elapsedMs,
  nodes,
  phase,
  schedule,
  ...props
}: WorkflowRunTimelineProps) => {
  const total = Math.max(schedule.totalMs, 1);
  const percent = (ms: number) => `${(Math.min(ms, total) / total) * 100}%`;

  return (
    <div
      className={cn("flex flex-col gap-1 text-xs", className)}
      data-slot="workflow-run-timeline"
      {...props}
    >
      {schedule.order.map((id) => {
        const node = nodes.find((item) => item.id === id);
        const run = schedule.nodes[id];
        if (!(node && run)) {
          return null;
        }
        const state = getNodeState(node, schedule, phase, elapsedMs);
        const live = state.status === "running" || state.status === "waiting";
        const shown = phase === "idle" ? run.end - run.start : state.elapsedMs;

        return (
          <div
            className="grid grid-cols-[minmax(0,10rem)_minmax(0,1fr)_3rem] items-center gap-3"
            data-slot="workflow-run-timeline-row"
            data-status={state.status}
            key={id}
          >
            <span className="flex min-w-0 items-center gap-2">
              <WorkflowBlockIcon
                block={node}
                className="size-5 rounded [&_svg]:size-3"
              />
              <span className="truncate font-medium">{node.title}</span>
            </span>
            <span className="bg-muted relative h-5 overflow-hidden rounded-md">
              {run.status === "skipped" ? (
                <span
                  className="border-muted-foreground/30 absolute inset-y-1.5 border-t border-dashed"
                  style={{ left: percent(run.start), right: 0 }}
                />
              ) : (
                <>
                  <span
                    className="bg-foreground/[0.06] absolute inset-y-1 rounded"
                    style={{
                      left: percent(run.start),
                      width: percent(run.end - run.start),
                    }}
                  />
                  <span
                    className={cn(
                      "absolute inset-y-1 rounded transition-[width] duration-75",
                      run.status === "error" ? "bg-red-500" : "bg-primary",
                      live && "motion-safe:animate-pulse",
                      phase === "idle" && "opacity-25"
                    )}
                    style={{ left: percent(run.start), width: percent(shown) }}
                  />
                </>
              )}
              {phase === "running" && (
                <span
                  aria-hidden="true"
                  className="bg-foreground/50 absolute inset-y-0 w-px"
                  style={{ left: percent(elapsedMs) }}
                />
              )}
            </span>
            <span className="text-muted-foreground text-right font-mono text-[0.6875rem] tabular-nums">
              {run.status === "skipped" ? "–" : formatDuration(shown)}
            </span>
          </div>
        );
      })}
    </div>
  );
};
```

### `registry/notra/workflow/components/workflow-shell.tsx`

```tsx
import { cn } from "cn";
import type { ComponentProps } from "react";

export const WorkflowShell = ({
  className,
  ...props
}: ComponentProps<"div">) => (
  <div
    className={cn(
      "border-table-shell-border bg-table-shell flex min-w-0 flex-col rounded-2xl border p-0.5",
      className
    )}
    data-slot="workflow-shell"
    {...props}
  />
);

export const WorkflowShellCard = ({
  className,
  ...props
}: ComponentProps<"div">) => (
  <div
    className={cn(
      "border-table-card-border bg-table-card shadow-table-card relative min-w-0 rounded-[14px] border",
      className
    )}
    data-slot="workflow-shell-card"
    {...props}
  />
);

export const WorkflowShellHeader = ({
  className,
  ...props
}: ComponentProps<"div">) => (
  <div
    className={cn(
      "flex min-h-10 min-w-0 flex-wrap items-center gap-2 px-3 py-1.5 text-sm font-medium",
      className
    )}
    data-slot="workflow-shell-header"
    {...props}
  />
);
```

### `registry/notra/workflow/components/workflow-status.tsx`

```tsx
import { cn } from "cn";
import {
  CheckIcon,
  CircleDashedIcon,
  CircleIcon,
  DicesIcon,
  HourglassIcon,
  LoaderCircleIcon,
  MinusIcon,
  RulerIcon,
  XIcon,
} from "lucide-react";

import { Badge } from "@/components/ui/badge";

import { WORKFLOW_STATUSES } from "../constants/workflow";
import {
  WORKFLOW_LED_CLASS,
  WORKFLOW_PILL_BOTTOM_CLASS,
  WORKFLOW_PILL_CLASS,
  WORKFLOW_PILL_TOP_CLASS,
} from "../constants/workflow-ui";
import type {
  WorkflowDeterminismBadgeProps,
  WorkflowStatusBadgeProps,
  WorkflowStatusGlyphProps,
  WorkflowStatusIndicatorProps,
  WorkflowStatusPillProps,
} from "../types/workflow";

const StatusGlyph = ({ status }: WorkflowStatusGlyphProps) => {
  switch (status) {
    case "running": {
      return <LoaderCircleIcon className="size-3.5 animate-spin" />;
    }
    case "waiting": {
      return <HourglassIcon className="size-3.5 motion-safe:animate-pulse" />;
    }
    case "success": {
      return <CheckIcon className="size-3.5" strokeWidth={2.5} />;
    }
    case "error": {
      return <XIcon className="size-3.5" strokeWidth={2.5} />;
    }
    case "skipped": {
      return <MinusIcon className="size-3.5" />;
    }
    case "queued": {
      return <CircleDashedIcon className="size-3.5" />;
    }
    default: {
      return <CircleIcon className="size-3.5" />;
    }
  }
};

export const WorkflowStatusIndicator = ({
  className,
  status,
  ...props
}: WorkflowStatusIndicatorProps) => (
  <span
    className={cn(
      "inline-flex size-4 shrink-0 items-center justify-center",
      WORKFLOW_STATUSES[status].className,
      className
    )}
    data-slot="workflow-status-indicator"
    data-status={status}
    {...props}
  >
    <StatusGlyph status={status} />
    <span className="sr-only">{WORKFLOW_STATUSES[status].label}</span>
  </span>
);

export const WorkflowStatusBadge = ({
  className,
  status,
  ...props
}: WorkflowStatusBadgeProps) => (
  <Badge
    className={cn("gap-1 pl-1", WORKFLOW_STATUSES[status].className, className)}
    data-slot="workflow-status-badge"
    data-status={status}
    variant="outline"
    {...props}
  >
    <StatusGlyph status={status} />
    {WORKFLOW_STATUSES[status].label}
  </Badge>
);

export const WorkflowDeterminismBadge = ({
  className,
  deterministic,
  ...props
}: WorkflowDeterminismBadgeProps) => (
  <Badge
    className={cn(
      "text-muted-foreground gap-1 font-normal",
      !deterministic &&
        "border-violet-500/30 text-violet-600 dark:text-violet-300",
      className
    )}
    data-slot="workflow-determinism-badge"
    title={
      deterministic
        ? "Same input, same output"
        : "Uses a language model, so the output can change between runs"
    }
    variant="outline"
    {...props}
  >
    {deterministic ? <RulerIcon /> : <DicesIcon />}
    {deterministic ? "Deterministic" : "Non-deterministic"}
  </Badge>
);

export const WorkflowStatusPill = ({
  className,
  status,
  ...props
}: WorkflowStatusPillProps) => (
  <span
    className={cn(WORKFLOW_PILL_CLASS, className)}
    data-slot="workflow-status-pill"
    data-status={status}
    title={WORKFLOW_STATUSES[status].label}
    {...props}
  >
    <span className={WORKFLOW_PILL_TOP_CLASS}>
      <span
        className={cn(
          WORKFLOW_LED_CLASS,
          WORKFLOW_STATUSES[status].ledClassName
        )}
      />
    </span>
    {status !== "idle" && (
      <span className={WORKFLOW_PILL_BOTTOM_CLASS}>
        <StatusGlyph status={status} />
      </span>
    )}
    <span className="sr-only">{WORKFLOW_STATUSES[status].label}</span>
  </span>
);
```

### `registry/notra/workflow/components/workflow-timer.tsx`

```tsx
import { cn } from "cn";

import { formatDuration } from "../lib/workflow";
import type { WorkflowTimerProps } from "../types/workflow";

export const WorkflowTimer = ({
  className,
  ms,
  running,
  ...props
}: WorkflowTimerProps) => (
  <span
    className={cn(
      "text-muted-foreground inline-flex items-center gap-1 font-mono text-[0.6875rem] leading-4 tabular-nums",
      running && "text-foreground",
      className
    )}
    data-running={running || undefined}
    data-slot="workflow-timer"
    {...props}
  >
    {running && (
      <span
        aria-hidden="true"
        className="bg-primary size-1.5 rounded-full motion-safe:animate-pulse"
      />
    )}
    <time dateTime={`PT${(ms / 1000).toFixed(1)}S`}>{formatDuration(ms)}</time>
  </span>
);
```

### `registry/notra/workflow/constants/workflow-runs.ts`

```ts
import type { WorkflowRunSummary } from "../types/workflow";

export const WORKFLOW_RUNS: WorkflowRunSummary[] = [
  {
    durationMs: 14_200,
    id: "Run #128",
    startedAt: "Today, 09:00",
    status: "success",
    steps: 11,
    trigger: "Schedule",
  },
  {
    durationMs: 16_100,
    id: "Run #127",
    startedAt: "Oct 20, 09:00",
    status: "success",
    steps: 11,
    trigger: "Schedule",
  },
  {
    durationMs: 6_400,
    id: "Run #126",
    startedAt: "Oct 17, 14:12",
    status: "error",
    steps: 5,
    trigger: "Manual",
  },
  {
    durationMs: 15_800,
    id: "Run #125",
    startedAt: "Oct 13, 09:00",
    status: "success",
    steps: 11,
    trigger: "Schedule",
  },
  {
    durationMs: 17_300,
    id: "Run #124",
    startedAt: "Oct 6, 09:00",
    status: "success",
    steps: 11,
    trigger: "Schedule",
  },
];
```

### `registry/notra/workflow/constants/workflow-templates.ts`

```ts
import type { WorkflowTemplate } from "../types/workflow";
import { WORKFLOW_COLUMN } from "./workflow-ui";

export const LISTICLE_OUTREACH_WORKFLOW: WorkflowTemplate = {
  category: "Outreach",
  description:
    "Finds best-of listicles that AI answers cite, checks whether you're on them and emails the owners of the ones you're missing from.",
  edges: [
    { id: "e1", source: "trigger", target: "search" },
    { id: "e2", source: "trigger", target: "backlinks" },
    { id: "e3", source: "search", target: "score", targetPort: "search" },
    { id: "e4", source: "backlinks", target: "score", targetPort: "backlinks" },
    { id: "e5", source: "score", target: "classify" },
    { id: "e6", source: "classify", target: "listed" },
    { id: "e7", source: "listed", sourcePort: "yes", target: "tracker" },
    { id: "e8", source: "listed", sourcePort: "no", target: "contact" },
    { id: "e9", source: "contact", target: "draft" },
    { id: "e10", source: "draft", target: "review" },
    { id: "e11", source: "review", target: "send" },
  ],
  id: "listicle-outreach",
  nodes: [
    {
      description: "Every Monday at 09:00",
      durationMs: 500,
      fields: [
        { label: "Schedule", value: "0 9 * * 1" },
        { label: "Timezone", value: "Europe/Berlin" },
      ],
      id: "trigger",
      kind: "trigger",
      position: { x: 24, y: 256 },
      title: "Weekly schedule",
    },
    {
      description: "Searches “best changelog tools” and 11 more prompts.",
      durationMs: 1800,
      fields: [
        { label: "Session", value: "Stealth, residential proxy" },
        { label: "Queries", value: "12" },
        { label: "Pages per query", value: "2" },
      ],
      id: "search",
      kind: "integration",
      logo: "browserbase",
      output: `[
  { "url": "https://saasbase.dev/best-changelog-tools", "rank": 2 },
  { "url": "https://devtools.fm/top-release-notes-apps", "rank": 4 }
]`,
      position: { x: 24 + WORKFLOW_COLUMN, y: 64 },
      title: "Search listicles",
    },
    {
      description: "Pages linking to 3 competitors.",
      durationMs: 1300,
      fields: [
        {
          label: "Competitors",
          value: "beamer.com, launchnotes.com, headway.app",
        },
        { label: "Min. domain rating", value: "40" },
      ],
      id: "backlinks",
      kind: "integration",
      logo: "ahrefs",
      position: { x: 24 + WORKFLOW_COLUMN, y: 448 },
      title: "Competitor backlinks",
    },
    {
      durationMs: 700,
      fields: [
        { label: "Dedupe by", value: "Canonical URL" },
        { label: "Score", value: "AI citations × domain rating" },
      ],
      id: "score",
      inputs: [
        { id: "search", label: "Search results" },
        { id: "backlinks", label: "Backlinks" },
      ],
      kind: "task",
      output: `{ "candidates": 37, "dropped": 14 }`,
      position: { x: 24 + WORKFLOW_COLUMN * 2, y: 256 },
      title: "Dedupe & score",
    },
    {
      description: "Is this a listicle we fit in?",
      durationMs: 2200,
      fields: [
        { label: "Temperature", value: "0.2" },
        { label: "Output", value: "{ fit: boolean, reason: string }" },
      ],
      id: "classify",
      kind: "ai",
      model: "claude-sonnet-5-5",
      output: `{ "fit": true, "reason": "Compares 9 changelog tools for B2B SaaS" }`,
      position: { x: 24 + WORKFLOW_COLUMN * 3, y: 256 },
      title: "Classify fit",
    },
    {
      branch: "no",
      durationMs: 400,
      fields: [{ label: "Condition", value: 'page.mentions("Notra")' }],
      id: "listed",
      kind: "condition",
      outputs: [
        {
          hint: "Update the tracker",
          id: "yes",
          label: "Already listed",
          rate: 0.38,
        },
        {
          hint: "Find the site owner and pitch",
          id: "no",
          label: "Not listed",
          rate: 0.62,
        },
      ],
      position: { x: 24 + WORKFLOW_COLUMN * 4, y: 256 },
      rule: { operator: "mentions", subject: "Listicle", value: "Notra" },
      title: "Are we listed?",
    },
    {
      description: "Marks the page as won.",
      durationMs: 600,
      id: "tracker",
      kind: "integration",
      logo: "notion",
      position: { x: 24 + WORKFLOW_COLUMN * 5, y: 64 },
      title: "Update tracker",
    },
    {
      description: "Finds the author's email on the site.",
      durationMs: 1600,
      fields: [
        { label: "Pages", value: "/about, /contact, author bio" },
        { label: "Fallback", value: "WHOIS, LinkedIn" },
      ],
      id: "contact",
      kind: "integration",
      logo: "firecrawl",
      output: `{ "name": "Maya Lindqvist", "email": "maya@example.com" }`,
      position: { x: 24 + WORKFLOW_COLUMN * 5, y: 448 },
      title: "Find site owner",
    },
    {
      description: "A short, personal pitch with one proof point.",
      durationMs: 2600,
      fields: [
        { label: "Temperature", value: "0.7" },
        { label: "Max length", value: "120 words" },
      ],
      id: "draft",
      kind: "ai",
      model: "gpt-5",
      position: { x: 24 + WORKFLOW_COLUMN * 6, y: 448 },
      title: "Draft pitch",
    },
    {
      description: "Waits for someone on the team.",
      durationMs: 1800,
      fields: [{ label: "Approvers", value: "#geo-outreach" }],
      id: "review",
      kind: "approval",
      position: { x: 24 + WORKFLOW_COLUMN * 7, y: 448 },
      title: "Review draft",
    },
    {
      description: "From your outreach inbox",
      durationMs: 900,
      fields: [
        { label: "Follow-ups", value: "Day 3, day 7" },
        { label: "Stop on", value: "Reply, bounce" },
      ],
      id: "send",
      kind: "email",
      logo: "resend",
      position: { x: 24 + WORKFLOW_COLUMN * 8, y: 448 },
      title: "Send outreach",
    },
  ],
  title: "Listicle outreach",
};

export const CONTENT_REFRESH_WORKFLOW: WorkflowTemplate = {
  category: "Content",
  description:
    "When a page loses AI visibility, diagnoses why and routes it to the right fix before republishing.",
  edges: [
    { id: "e1", source: "drop", target: "fetch" },
    { id: "e2", source: "fetch", target: "diagnose" },
    { id: "e3", source: "diagnose", target: "route" },
    { id: "e4", source: "route", sourcePort: "stale", target: "rewrite" },
    { id: "e5", source: "route", sourcePort: "schema", target: "jsonld" },
    { id: "e6", source: "route", sourcePort: "blocked", target: "robots" },
    { id: "e7", source: "route", sourcePort: "competitor", target: "compare" },
    { id: "e8", source: "rewrite", target: "merge" },
    { id: "e9", source: "jsonld", target: "merge" },
    { id: "e10", source: "robots", target: "merge" },
    { id: "e11", source: "compare", target: "merge" },
    { id: "e12", source: "merge", target: "publish" },
  ],
  id: "content-refresh",
  nodes: [
    {
      description: "Share of voice fell 20% in 7 days.",
      durationMs: 500,
      id: "drop",
      kind: "trigger",
      position: { x: 24, y: 296 },
      title: "Visibility dropped",
    },
    {
      durationMs: 1200,
      id: "fetch",
      kind: "integration",
      logo: "context-dev",
      position: { x: 24 + WORKFLOW_COLUMN, y: 296 },
      title: "Fetch page",
    },
    {
      description: "Compares the page with the answers that dropped it.",
      durationMs: 2400,
      id: "diagnose",
      kind: "ai",
      model: "gemini-2-5-pro",
      position: { x: 24 + WORKFLOW_COLUMN * 2, y: 296 },
      title: "Diagnose drop",
    },
    {
      branch: "stale",
      durationMs: 400,
      id: "route",
      kind: "switch",
      outputs: [
        {
          hint: "Rewrite the stale sections",
          id: "stale",
          label: "Outdated facts",
          rate: 0.41,
        },
        {
          hint: "Add structured data",
          id: "schema",
          label: "Missing schema",
          rate: 0.23,
        },
        {
          hint: "Unblock AI crawlers",
          id: "blocked",
          label: "Crawler blocked",
          rate: 0.12,
        },
        {
          hint: "Publish a comparison page",
          id: "competitor",
          label: "Competitor overtook",
          rate: 0.24,
        },
      ],
      position: { x: 24 + WORKFLOW_COLUMN * 3, y: 296 },
      title: "Route by cause",
    },
    {
      durationMs: 2600,
      id: "rewrite",
      kind: "ai",
      model: "claude-opus-5-5",
      position: { x: 24 + WORKFLOW_COLUMN * 4, y: 0 },
      title: "Rewrite stale sections",
    },
    {
      description: "Adds FAQPage and Product JSON-LD.",
      durationMs: 600,
      id: "jsonld",
      kind: "task",
      position: { x: 24 + WORKFLOW_COLUMN * 4, y: 192 },
      title: "Generate schema",
    },
    {
      durationMs: 900,
      id: "robots",
      kind: "integration",
      logo: "github",
      position: { x: 24 + WORKFLOW_COLUMN * 4, y: 427.5 },
      title: "Open robots.txt PR",
    },
    {
      durationMs: 2400,
      id: "compare",
      kind: "ai",
      model: "gpt-5",
      position: { x: 24 + WORKFLOW_COLUMN * 4, y: 579 },
      title: "Write comparison page",
    },
    {
      description: "Continues when any fix is ready.",
      durationMs: 300,
      id: "merge",
      kind: "merge",
      position: { x: 24 + WORKFLOW_COLUMN * 5, y: 296 },
      title: "Collect fixes",
    },
    {
      durationMs: 1100,
      id: "publish",
      kind: "integration",
      logo: "webflow",
      position: { x: 24 + WORKFLOW_COLUMN * 6, y: 296 },
      title: "Publish update",
    },
  ],
  title: "Content refresh",
};

export const VISIBILITY_MONITOR_WORKFLOW: WorkflowTemplate = {
  category: "Monitoring",
  description:
    "Asks four answer engines your tracked prompts every day and alerts the team when you lose a citation.",
  edges: [
    { id: "e1", source: "daily", target: "prompts" },
    { id: "e2", source: "prompts", target: "chatgpt" },
    { id: "e3", source: "prompts", target: "perplexity" },
    { id: "e4", source: "prompts", target: "gemini" },
    { id: "e5", source: "prompts", target: "grok" },
    { id: "e6", source: "chatgpt", target: "diff" },
    { id: "e7", source: "perplexity", target: "diff" },
    { id: "e8", source: "gemini", target: "diff" },
    { id: "e9", source: "grok", target: "diff" },
    { id: "e10", source: "diff", target: "lost" },
    { id: "e11", source: "lost", sourcePort: "true", target: "slack" },
    { id: "e12", source: "lost", sourcePort: "true", target: "linear" },
    { id: "e13", source: "lost", sourcePort: "false", target: "sheet" },
  ],
  id: "visibility-monitor",
  nodes: [
    {
      description: "Every day at 06:00",
      durationMs: 400,
      id: "daily",
      kind: "trigger",
      position: { x: 24, y: 296 },
      title: "Daily",
    },
    {
      description: "48 tracked prompts in 3 languages.",
      durationMs: 500,
      id: "prompts",
      kind: "loop",
      position: { x: 24 + WORKFLOW_COLUMN, y: 296 },
      title: "For each prompt",
    },
    {
      durationMs: 2000,
      id: "chatgpt",
      kind: "ai",
      model: "gpt-5",
      position: { x: 24 + WORKFLOW_COLUMN * 2, y: 0 },
      title: "Ask ChatGPT",
    },
    {
      durationMs: 1500,
      id: "perplexity",
      kind: "ai",
      model: "sonar-pro",
      position: { x: 24 + WORKFLOW_COLUMN * 2, y: 192 },
      title: "Ask Perplexity",
    },
    {
      durationMs: 1800,
      id: "gemini",
      kind: "ai",
      model: "gemini-2-5-pro",
      position: { x: 24 + WORKFLOW_COLUMN * 2, y: 384 },
      title: "Ask Gemini",
    },
    {
      durationMs: 1700,
      id: "grok",
      kind: "ai",
      model: "grok-4",
      position: { x: 24 + WORKFLOW_COLUMN * 2, y: 576 },
      title: "Ask Grok",
    },
    {
      description: "Compares citations with yesterday.",
      durationMs: 700,
      id: "diff",
      kind: "task",
      position: { x: 24 + WORKFLOW_COLUMN * 3, y: 280 },
      title: "Diff citations",
    },
    {
      branch: "true",
      durationMs: 300,
      id: "lost",
      kind: "condition",
      outputs: [
        {
          hint: "Alert the team",
          id: "true",
          label: "Lost a citation",
          rate: 0.17,
        },
        {
          hint: "Log it and move on",
          id: "false",
          label: "No change",
          rate: 0.83,
        },
      ],
      position: { x: 24 + WORKFLOW_COLUMN * 4, y: 280 },
      rule: { operator: "lost", subject: "Any engine", value: "a citation" },
      title: "Lost citations?",
    },
    {
      durationMs: 600,
      id: "slack",
      kind: "integration",
      logo: "slack",
      position: { x: 24 + WORKFLOW_COLUMN * 5, y: 123.5 },
      title: "Alert #geo",
    },
    {
      durationMs: 800,
      id: "linear",
      kind: "integration",
      logo: "linear",
      position: { x: 24 + WORKFLOW_COLUMN * 5, y: 368.5 },
      title: "Create issue",
    },
    {
      durationMs: 500,
      id: "sheet",
      kind: "integration",
      logo: "google-sheets",
      position: { x: 24 + WORKFLOW_COLUMN * 5, y: 523.5 },
      title: "Log to sheet",
    },
  ],
  title: "AI visibility monitor",
};

export const SCHEMA_AUDIT_WORKFLOW: WorkflowTemplate = {
  category: "Technical",
  description:
    "Crawls the sitemap, validates structured data on every page and opens one PR with the fixes.",
  edges: [
    { id: "e1", source: "deploy", target: "crawl" },
    { id: "e2", source: "crawl", target: "validate" },
    { id: "e3", source: "validate", target: "fix" },
    { id: "e4", source: "fix", target: "pr" },
  ],
  id: "schema-audit",
  nodes: [
    {
      durationMs: 400,
      id: "deploy",
      kind: "trigger",
      position: { x: 24, y: 64 },
      title: "On deploy",
    },
    {
      durationMs: 1600,
      id: "crawl",
      kind: "integration",
      logo: "context-dev",
      position: { x: 24 + WORKFLOW_COLUMN, y: 64 },
      title: "Render pages",
    },
    {
      durationMs: 800,
      id: "validate",
      kind: "task",
      position: { x: 24 + WORKFLOW_COLUMN * 2, y: 64 },
      title: "Validate JSON-LD",
    },
    {
      durationMs: 2200,
      id: "fix",
      kind: "ai",
      model: "claude-sonnet-5-5",
      position: { x: 24 + WORKFLOW_COLUMN * 3, y: 64 },
      title: "Write fixes",
    },
    {
      durationMs: 900,
      id: "pr",
      kind: "integration",
      logo: "github",
      position: { x: 24 + WORKFLOW_COLUMN * 4, y: 64 },
      title: "Open pull request",
    },
  ],
  title: "Schema audit",
};

export const COMMUNITY_MENTIONS_WORKFLOW: WorkflowTemplate = {
  category: "Monitoring",
  description:
    "Watches threads that answer engines quote, drafts a helpful reply and waits for a human before posting.",
  edges: [
    { id: "e1", source: "hourly", target: "find" },
    { id: "e2", source: "find", target: "relevant" },
    { id: "e3", source: "relevant", sourcePort: "yes", target: "reply" },
    { id: "e4", source: "reply", target: "approve" },
  ],
  id: "community-mentions",
  nodes: [
    {
      durationMs: 400,
      id: "hourly",
      kind: "trigger",
      position: { x: 24, y: 64 },
      title: "Hourly",
    },
    {
      durationMs: 1400,
      id: "find",
      kind: "ai",
      model: "sonar-pro",
      position: { x: 24 + WORKFLOW_COLUMN, y: 64 },
      title: "Find cited threads",
    },
    {
      branch: "yes",
      durationMs: 300,
      id: "relevant",
      kind: "condition",
      outputs: [
        {
          hint: "Draft a helpful reply",
          id: "yes",
          label: "Relevant",
          rate: 0.3,
        },
        { hint: "Skip the thread", id: "no", label: "Ignore", rate: 0.7 },
      ],
      position: { x: 24 + WORKFLOW_COLUMN * 2, y: 64 },
      rule: {
        operator: "asks about",
        subject: "Thread",
        value: "changelog tools",
      },
      title: "Worth replying?",
    },
    {
      durationMs: 2000,
      id: "reply",
      kind: "ai",
      model: "claude-haiku-5-5",
      position: { x: 24 + WORKFLOW_COLUMN * 3, y: 152.5 },
      title: "Draft reply",
    },
    {
      durationMs: 1200,
      id: "approve",
      kind: "approval",
      position: { x: 24 + WORKFLOW_COLUMN * 4, y: 152.5 },
      title: "Approve reply",
    },
  ],
  title: "Community mentions",
};

export const WEEKLY_DIGEST_WORKFLOW: WorkflowTemplate = {
  category: "Reporting",
  description:
    "Summarizes the week's AI visibility, crawler traffic and won listicles into one email for the team.",
  edges: [
    { id: "e1", source: "friday", target: "traffic" },
    { id: "e2", source: "friday", target: "visibility" },
    { id: "e3", source: "traffic", target: "summary", targetPort: "traffic" },
    {
      id: "e4",
      source: "visibility",
      target: "summary",
      targetPort: "visibility",
    },
    { id: "e5", source: "summary", target: "email" },
  ],
  id: "weekly-digest",
  nodes: [
    {
      durationMs: 400,
      id: "friday",
      kind: "trigger",
      position: { x: 24, y: 160 },
      title: "Fridays at 16:00",
    },
    {
      durationMs: 900,
      id: "traffic",
      kind: "integration",
      logo: "posthog",
      position: { x: 24 + WORKFLOW_COLUMN, y: 32 },
      title: "AI referral traffic",
    },
    {
      durationMs: 1000,
      id: "visibility",
      kind: "task",
      position: { x: 24 + WORKFLOW_COLUMN, y: 272 },
      title: "Visibility report",
    },
    {
      durationMs: 2100,
      id: "summary",
      inputs: [
        { id: "traffic", label: "Traffic" },
        { id: "visibility", label: "Visibility" },
      ],
      kind: "ai",
      model: "claude-haiku-5-5",
      position: { x: 24 + WORKFLOW_COLUMN * 2, y: 112 },
      title: "Write digest",
    },
    {
      durationMs: 700,
      id: "email",
      kind: "email",
      logo: "gmail",
      position: { x: 24 + WORKFLOW_COLUMN * 3, y: 112 },
      title: "Email the team",
    },
  ],
  title: "Weekly GEO digest",
};

export const WORKFLOW_TEMPLATES: WorkflowTemplate[] = [
  LISTICLE_OUTREACH_WORKFLOW,
  CONTENT_REFRESH_WORKFLOW,
  VISIBILITY_MONITOR_WORKFLOW,
  SCHEMA_AUDIT_WORKFLOW,
  COMMUNITY_MENTIONS_WORKFLOW,
  WEEKLY_DIGEST_WORKFLOW,
];
```

### `registry/notra/workflow/constants/workflow-ui.ts`

```ts
import type {
  WorkflowBannerConfig,
  WorkflowEdgeStatus,
  WorkflowRunPhase,
} from "../types/workflow";

export const WORKFLOW_NODE_WIDTH = 256;

export const WORKFLOW_NODE_DROP_OFFSET_Y = 24;

export const WORKFLOW_COLUMN = 320;

export const WORKFLOW_FIT_PADDING = 0.12;

export const WORKFLOW_FIT_MIN_ZOOM = 0.6;

export const WORKFLOW_FIT_MAX_ZOOM = 1;

export const WORKFLOW_FIT_EDGE_PX = 32;

export const WORKFLOW_FIT_VIEW_OPTIONS = {
  maxZoom: WORKFLOW_FIT_MAX_ZOOM,
  padding: WORKFLOW_FIT_PADDING,
};

export const WORKFLOW_ZOOM_DURATION_MS = 200;

export const WORKFLOW_FIT_DURATION_MS = 300;

export const WORKFLOW_MINIMAP_STYLE = { height: 96, width: 144 };

export const WORKFLOW_PATTERN_STRENGTH = {
  cross: 22,
  dots: 24,
  gridMajor: 12,
  gridMinor: 5,
  lines: 7,
};

export const WORKFLOW_PORT_ROW_CLASS =
  "text-muted-foreground border-table-card-border relative flex h-7.5 min-w-0 items-center border-t px-3 text-[0.6875rem] font-medium";

export const WORKFLOW_CHIP_CLASS =
  "bg-foreground/[0.05] text-muted-foreground inline-flex h-5 min-w-0 items-center gap-1 rounded-full px-2 text-[0.6875rem] leading-none tabular-nums [&_svg]:size-3 [&_svg]:shrink-0";

export const WORKFLOW_BANNERS: Record<
  "error" | "waiting",
  WorkflowBannerConfig
> = {
  error: {
    className: "text-red-600 dark:text-red-400",
    label: "Failed",
    message: "Check the run log",
  },
  waiting: {
    className: "text-amber-600 dark:text-amber-400",
    label: "Waiting",
    message: "Needs approval",
  },
};

export const WORKFLOW_PILL_CLASS =
  "bg-table-shell border-table-shell-border text-muted-foreground inline-flex h-9 w-7 shrink-0 flex-col overflow-hidden rounded-lg border [&_svg]:size-3";

export const WORKFLOW_PILL_TOP_CLASS =
  "flex flex-1 items-center justify-center";

export const WORKFLOW_PILL_BOTTOM_CLASS =
  "border-table-shell-border flex flex-1 items-center justify-center border-t";

export const WORKFLOW_LED_CLASS =
  "ring-background/60 size-1.5 rounded-full ring-2 transition-colors duration-300";

export const WORKFLOW_EDGE_COLOR: Record<WorkflowEdgeStatus, string> = {
  active: "text-primary",
  done: "text-primary/70",
  idle: "text-muted-foreground/70",
  skipped: "text-muted-foreground/25",
};

export const WORKFLOW_EDGE_DASH = "0 10";

export const WORKFLOW_EDGE_STEP_OPTIONS = { borderRadius: 24, offset: 24 };

export const WORKFLOW_EDGE_RADIUS = 24;

export const WORKFLOW_RUN_LABELS: Record<WorkflowRunPhase, string> = {
  done: "Run again",
  idle: "Run workflow",
  running: "Stop",
};

export const WORKFLOW_RUN_RING_RADIUS = 10;

export const WORKFLOW_RUN_RING_LENGTH = 2 * Math.PI * WORKFLOW_RUN_RING_RADIUS;

export const WORKFLOW_SECTION_TITLE_CLASS =
  "text-muted-foreground text-[0.6875rem] font-medium tracking-wide uppercase";

export const WORKFLOW_EDGE_REVERSE_STYLE = { animationDirection: "reverse" };

export const WORKFLOW_SKIPPED_DIM_CLASS =
  "transition-opacity duration-300 group-data-[status=skipped]/node:opacity-45";

export const WORKFLOW_BRANCH_RAIL_CLASS =
  "border-table-card-border bg-table-card/60 divide-table-card-border mt-0.5 divide-y rounded-[14px] border";

export const WORKFLOW_BRANCH_ROW_CLASS =
  "relative flex h-10 min-w-0 items-center gap-2.5 px-3";

export const WORKFLOW_BRANCH_LED_CLASS =
  "size-2 shrink-0 rounded-full ring-2 transition-[background-color,box-shadow,opacity] duration-300";

export const WORKFLOW_RULE_TOKEN_CLASS =
  "bg-foreground/[0.05] text-foreground/80 inline-flex h-5 max-w-full min-w-0 items-center truncate rounded-md px-1.5 text-[0.6875rem] whitespace-nowrap";
```

### `registry/notra/workflow/constants/workflow.ts`

```ts
import {
  FlagIcon,
  GitForkIcon,
  HourglassIcon,
  MailIcon,
  MergeIcon,
  PlugIcon,
  RepeatIcon,
  SparklesIcon,
  SplitIcon,
  SquareFunctionIcon,
  UserCheckIcon,
  ZapIcon,
} from "lucide-react";

import type {
  WorkflowKindConfig,
  WorkflowModel,
  WorkflowNodeKind,
  WorkflowNodeStatus,
  WorkflowPort,
  WorkflowStatusConfig,
} from "../types/workflow";

export const WORKFLOW_DEFAULT_DURATION_MS = 900;

export const WORKFLOW_EDGE_TRAVEL_MS = 450;

export const WORKFLOW_TICK_MS = 50;

export const WORKFLOW_BLOCK_MIME = "application/x-workflow-block";

export const WORKFLOW_IN_PORT: WorkflowPort = { id: "in" };

export const WORKFLOW_OUT_PORT: WorkflowPort = { id: "out" };

export const WORKFLOW_KINDS: Record<WorkflowNodeKind, WorkflowKindConfig> = {
  ai: {
    icon: SparklesIcon,
    label: "AI step",
    tileClassName:
      "bg-violet-500/10 text-violet-600 ring-violet-500/20 dark:text-violet-300",
  },
  approval: {
    icon: UserCheckIcon,
    label: "Human approval",
    tileClassName:
      "bg-pink-500/10 text-pink-600 ring-pink-500/20 dark:text-pink-300",
  },
  condition: {
    icon: SplitIcon,
    label: "If / else",
    tileClassName:
      "bg-orange-500/10 text-orange-600 ring-orange-500/20 dark:text-orange-300",
  },
  delay: {
    icon: HourglassIcon,
    label: "Wait",
    tileClassName:
      "bg-zinc-500/10 text-zinc-600 ring-zinc-500/20 dark:text-zinc-300",
  },
  email: {
    icon: MailIcon,
    label: "Email",
    tileClassName:
      "bg-emerald-500/10 text-emerald-600 ring-emerald-500/20 dark:text-emerald-300",
  },
  integration: {
    icon: PlugIcon,
    label: "Integration",
    tileClassName:
      "bg-sky-500/10 text-sky-600 ring-sky-500/20 dark:text-sky-300",
  },
  loop: {
    icon: RepeatIcon,
    label: "For each",
    tileClassName:
      "bg-indigo-500/10 text-indigo-600 ring-indigo-500/20 dark:text-indigo-300",
  },
  merge: {
    icon: MergeIcon,
    label: "Merge",
    tileClassName:
      "bg-teal-500/10 text-teal-600 ring-teal-500/20 dark:text-teal-300",
  },
  output: {
    icon: FlagIcon,
    label: "Output",
    tileClassName:
      "bg-zinc-500/10 text-zinc-600 ring-zinc-500/20 dark:text-zinc-300",
  },
  switch: {
    icon: GitForkIcon,
    label: "Decision",
    tileClassName:
      "bg-orange-500/10 text-orange-600 ring-orange-500/20 dark:text-orange-300",
  },
  task: {
    icon: SquareFunctionIcon,
    label: "Task",
    tileClassName:
      "bg-blue-500/10 text-blue-600 ring-blue-500/20 dark:text-blue-300",
  },
  trigger: {
    icon: ZapIcon,
    label: "Trigger",
    tileClassName:
      "bg-amber-500/10 text-amber-600 ring-amber-500/20 dark:text-amber-300",
  },
};

export const WORKFLOW_NON_DETERMINISTIC_KINDS: WorkflowNodeKind[] = ["ai"];

export const WORKFLOW_STATUSES: Record<
  WorkflowNodeStatus,
  WorkflowStatusConfig
> = {
  error: {
    className: "text-red-600 dark:text-red-400",
    label: "Failed",
    ledClassName: "bg-red-500 shadow-[0_0_6px_var(--color-red-500)]",
  },
  idle: {
    className: "text-muted-foreground",
    label: "Idle",
    ledClassName: "bg-muted-foreground/35",
  },
  queued: {
    className: "text-muted-foreground",
    label: "Queued",
    ledClassName: "bg-muted-foreground/35",
  },
  running: {
    className: "text-primary",
    label: "Running",
    ledClassName:
      "bg-primary shadow-[0_0_6px_var(--color-primary,var(--primary))] motion-safe:animate-pulse",
  },
  skipped: {
    className: "text-muted-foreground",
    label: "Skipped",
    ledClassName: "bg-muted-foreground/20",
  },
  success: {
    className: "text-emerald-600 dark:text-emerald-400",
    label: "Completed",
    ledClassName: "bg-emerald-500 shadow-[0_0_6px_var(--color-emerald-500)]",
  },
  waiting: {
    className: "text-amber-600 dark:text-amber-400",
    label: "Waiting for approval",
    ledClassName:
      "bg-amber-500 shadow-[0_0_6px_var(--color-amber-500)] motion-safe:animate-pulse",
  },
};

export const WORKFLOW_NODE_RING: Record<WorkflowNodeStatus, string> = {
  error: "border-red-500/60",
  idle: "",
  queued: "",
  running:
    "border-primary shadow-[0_0_0_0.3rem_color-mix(in_oklab,var(--color-primary,var(--primary))_14%,transparent)]",
  skipped: "",
  success: "border-emerald-500/50",
  waiting:
    "border-amber-500 shadow-[0_0_0_0.3rem_color-mix(in_oklab,var(--color-amber-500)_14%,transparent)]",
};

export const WORKFLOW_MODELS: WorkflowModel[] = [
  {
    id: "claude-opus-5-5",
    label: "Claude Opus 5.5",
    logo: "claude",
    provider: "Anthropic",
  },
  {
    id: "claude-sonnet-5-5",
    label: "Claude Sonnet 5.5",
    logo: "claude",
    provider: "Anthropic",
  },
  {
    id: "claude-haiku-5-5",
    label: "Claude Haiku 5.5",
    logo: "claude",
    provider: "Anthropic",
  },
  { id: "gpt-5", label: "GPT-5", logo: "openai", provider: "OpenAI" },
  { id: "gpt-5-mini", label: "GPT-5 mini", logo: "openai", provider: "OpenAI" },
  {
    id: "gemini-2-5-pro",
    label: "Gemini 2.5 Pro",
    logo: "gemini",
    provider: "Google",
  },
  {
    id: "sonar-pro",
    label: "Sonar Pro",
    logo: "perplexity",
    provider: "Perplexity",
  },
  { id: "grok-4", label: "Grok 4", logo: "grok", provider: "xAI" },
  {
    id: "mistral-large",
    label: "Mistral Large",
    logo: "mistral",
    provider: "Mistral",
  },
  {
    id: "deepseek-v3",
    label: "DeepSeek V3",
    logo: "deepseek",
    provider: "DeepSeek",
  },
];
```

### `registry/notra/workflow/examples/workflow-runs.tsx`

```tsx
"use client";

import { useMemo, useState } from "react";

import { WorkflowRunButton } from "../components/workflow-run-button";
import { WorkflowRunHistory } from "../components/workflow-run-history";
import { WorkflowRunLog } from "../components/workflow-run-log";
import { WorkflowRunTimeline } from "../components/workflow-run-timeline";
import {
  WorkflowShell,
  WorkflowShellCard,
  WorkflowShellHeader,
} from "../components/workflow-shell";
import { WorkflowStatusBadge } from "../components/workflow-status";
import { WorkflowTimer } from "../components/workflow-timer";
import { WORKFLOW_RUNS } from "../constants/workflow-runs";
import { LISTICLE_OUTREACH_WORKFLOW } from "../constants/workflow-templates";
import { useWorkflowRun } from "../hooks/use-workflow-run";
import { getRunLog, getRunOrder } from "../lib/workflow";
import type { WorkflowNodeStatus } from "../types/workflow";

const { edges } = LISTICLE_OUTREACH_WORKFLOW;
const ORDER = getRunOrder(LISTICLE_OUTREACH_WORKFLOW.nodes, edges);

export default function WorkflowRuns() {
  const [selected, setSelected] = useState(WORKFLOW_RUNS[0]?.id);
  const summary = WORKFLOW_RUNS.find((item) => item.id === selected);
  const failAt = summary?.status === "error" ? ORDER[summary.steps - 1] : "";
  const nodes = useMemo(
    () =>
      LISTICLE_OUTREACH_WORKFLOW.nodes.map((node) =>
        node.id === failAt ? { ...node, fail: true } : node
      ),
    [failAt]
  );
  const run = useWorkflowRun(nodes, edges);
  const log = getRunLog(nodes, run.schedule, run.phase, run.elapsedMs);
  let status: WorkflowNodeStatus = summary?.status ?? "idle";
  if (run.phase === "running") {
    status = "running";
  } else if (run.phase === "done") {
    status = failAt ? "error" : "success";
  }

  return (
    <WorkflowShell className="w-full">
      <WorkflowShellHeader className="gap-3">
        <span className="min-w-0 truncate">
          {LISTICLE_OUTREACH_WORKFLOW.title} · {selected}
        </span>
        <WorkflowStatusBadge status={status} />
        <div className="ml-auto flex items-center gap-3">
          <WorkflowTimer ms={run.elapsedMs} running={run.phase === "running"} />
          <WorkflowRunButton
            onPlay={run.play}
            onStop={run.stop}
            phase={run.phase}
            progress={run.elapsedMs / run.totalMs}
          />
        </div>
      </WorkflowShellHeader>
      <WorkflowShellCard className="grid overflow-hidden md:grid-cols-[17rem_minmax(0,1fr)]">
        <aside className="border-table-card-border flex min-w-0 flex-col gap-1 border-b p-1.5 md:border-r md:border-b-0">
          <h3 className="text-muted-foreground px-2.5 pt-1.5 pb-1 text-xs font-medium">
            Runs
          </h3>
          <WorkflowRunHistory
            onSelect={(id) => {
              run.stop();
              setSelected(id);
            }}
            runs={WORKFLOW_RUNS}
            selectedId={selected}
          />
        </aside>
        <section className="flex min-w-0 flex-col">
          <WorkflowRunTimeline
            className="border-table-card-border border-b p-4"
            elapsedMs={run.elapsedMs}
            nodes={nodes}
            phase={run.phase}
            schedule={run.schedule}
          />
          <WorkflowRunLog className="h-56 overflow-y-auto py-2" entries={log} />
        </section>
      </WorkflowShellCard>
    </WorkflowShell>
  );
}
```

### `registry/notra/workflow/hooks/use-workflow-run.ts`

```ts
"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { WORKFLOW_TICK_MS } from "../constants/workflow";
import { getEdgeState, getNodeState, scheduleRun } from "../lib/workflow";
import type {
  WorkflowEdgeData,
  WorkflowNodeData,
  WorkflowRun,
  WorkflowRunPhase,
} from "../types/workflow";

export const useWorkflowRun = (
  nodes: WorkflowNodeData[],
  edges: WorkflowEdgeData[]
): WorkflowRun => {
  const liveSchedule = useMemo(() => scheduleRun(nodes, edges), [nodes, edges]);
  const [phase, setPhase] = useState<WorkflowRunPhase>("idle");
  const [runSchedule, setRunSchedule] = useState(liveSchedule);
  const schedule = phase === "idle" ? liveSchedule : runSchedule;
  const [tickMs, setElapsedMs] = useState(0);
  const elapsedMs = phase === "done" ? schedule.totalMs : tickMs;
  const startedAt = useRef(0);

  useEffect(() => {
    if (phase !== "running") {
      return;
    }
    const id = window.setInterval(() => {
      const elapsed = performance.now() - startedAt.current;
      if (elapsed >= schedule.totalMs) {
        setElapsedMs(schedule.totalMs);
        setPhase("done");
        return;
      }
      setElapsedMs(elapsed);
    }, WORKFLOW_TICK_MS);
    return () => window.clearInterval(id);
  }, [phase, schedule.totalMs]);

  const play = useCallback(() => {
    setRunSchedule(liveSchedule);
    startedAt.current = performance.now();
    setElapsedMs(0);
    setPhase("running");
  }, [liveSchedule]);

  const stop = useCallback(() => {
    setElapsedMs(0);
    setPhase("idle");
  }, []);

  const nodeState = useCallback(
    (id: string) =>
      getNodeState(
        nodes.find((node) => node.id === id),
        schedule,
        phase,
        elapsedMs
      ),
    [nodes, schedule, phase, elapsedMs]
  );

  const edgeState = useCallback(
    (id: string) => getEdgeState(id, schedule, phase, elapsedMs),
    [schedule, phase, elapsedMs]
  );

  return {
    edgeState,
    elapsedMs,
    nodeState,
    phase,
    play,
    schedule,
    stop,
    totalMs: schedule.totalMs,
  };
};
```

### `registry/notra/workflow/lib/workflow.ts`

```ts
import { getSmoothStepPath } from "@xyflow/react";

import {
  WORKFLOW_DEFAULT_DURATION_MS,
  WORKFLOW_EDGE_TRAVEL_MS,
  WORKFLOW_IN_PORT,
  WORKFLOW_MODELS,
  WORKFLOW_NON_DETERMINISTIC_KINDS,
  WORKFLOW_OUT_PORT,
} from "../constants/workflow";
import {
  WORKFLOW_EDGE_RADIUS,
  WORKFLOW_EDGE_STEP_OPTIONS,
} from "../constants/workflow-ui";
import type {
  WorkflowAnchor,
  WorkflowBlock,
  WorkflowEdgeData,
  WorkflowEdgeStatus,
  WorkflowLogEntry,
  WorkflowNodeData,
  WorkflowNodeState,
  WorkflowPoint,
  WorkflowPort,
  WorkflowRunPhase,
  WorkflowSchedule,
} from "../types/workflow";

export const getInputPorts = (node: WorkflowNodeData): WorkflowPort[] =>
  node.inputs ?? (node.kind === "trigger" ? [] : [WORKFLOW_IN_PORT]);

export const getOutputPorts = (node: WorkflowNodeData): WorkflowPort[] =>
  node.outputs ?? (node.kind === "output" ? [] : [WORKFLOW_OUT_PORT]);

export const isDeterministic = (node: Pick<WorkflowNodeData, "kind">) =>
  !WORKFLOW_NON_DETERMINISTIC_KINDS.includes(node.kind);

export const findModel = (id?: string) =>
  WORKFLOW_MODELS.find((model) => model.id === id);

const byPosition = (a: WorkflowNodeData, b: WorkflowNodeData) =>
  a.position.x - b.position.x || a.position.y - b.position.y;

export const getRunOrder = (
  nodes: WorkflowNodeData[],
  edges: WorkflowEdgeData[]
) => {
  const indegree = new Map(nodes.map((node) => [node.id, 0]));
  for (const edge of edges) {
    if (indegree.has(edge.target) && indegree.has(edge.source)) {
      indegree.set(edge.target, (indegree.get(edge.target) ?? 0) + 1);
    }
  }

  const byId = new Map(nodes.map((node) => [node.id, node]));
  const ready = nodes.filter((node) => indegree.get(node.id) === 0);
  const order: string[] = [];
  while (ready.length > 0) {
    ready.sort(byPosition);
    const node = ready.shift();
    if (!node) {
      break;
    }
    order.push(node.id);
    for (const edge of edges) {
      if (edge.source !== node.id) {
        continue;
      }
      const left = (indegree.get(edge.target) ?? 0) - 1;
      indegree.set(edge.target, left);
      const target = byId.get(edge.target);
      if (left === 0 && target) {
        ready.push(target);
      }
    }
  }

  const ordered = new Set(order);
  const rest = nodes.filter((node) => !ordered.has(node.id)).sort(byPosition);
  return [...order, ...rest.map((node) => node.id)];
};

export const scheduleRun = (
  nodes: WorkflowNodeData[],
  edges: WorkflowEdgeData[]
): WorkflowSchedule => {
  const order = getRunOrder(nodes, edges);
  const schedule: WorkflowSchedule = {
    edges: {},
    nodes: {},
    order,
    totalMs: 0,
  };

  const byId = new Map(nodes.map((node) => [node.id, node]));
  for (const id of order) {
    const node = byId.get(id);
    if (!node) {
      continue;
    }
    const incoming = edges
      .filter((edge) => edge.target === id)
      .map((edge) => schedule.edges[edge.id])
      .filter((run) => run !== undefined);
    const taken = incoming.filter((run) => run.taken);
    const settled = Math.max(0, ...incoming.map((run) => run.end));

    if (incoming.length > 0 && taken.length === 0) {
      schedule.nodes[id] = { end: settled, start: settled, status: "skipped" };
    } else {
      const start = Math.max(0, ...taken.map((run) => run.end));
      schedule.nodes[id] = {
        end: start + (node.durationMs ?? WORKFLOW_DEFAULT_DURATION_MS),
        start,
        status: node.fail ? "error" : "success",
      };
    }

    const run = schedule.nodes[id];
    for (const edge of edges.filter((item) => item.source === id)) {
      const port = edge.sourcePort ?? WORKFLOW_OUT_PORT.id;
      const isTaken =
        run.status === "success" &&
        (node.branch === undefined || node.branch === port);
      schedule.edges[edge.id] = {
        end: run.end + (isTaken ? WORKFLOW_EDGE_TRAVEL_MS : 0),
        start: run.end,
        taken: isTaken,
      };
    }
    schedule.totalMs = Math.max(schedule.totalMs, run.end);
  }

  return schedule;
};

export const getNodeState = (
  node: WorkflowNodeData | undefined,
  schedule: WorkflowSchedule,
  phase: WorkflowRunPhase,
  elapsedMs: number
): WorkflowNodeState => {
  const run = node ? schedule.nodes[node.id] : undefined;
  if (!(node && run) || phase === "idle") {
    return { elapsedMs: 0, status: "idle" };
  }
  if (elapsedMs < run.start) {
    return { elapsedMs: 0, status: "queued" };
  }
  if (run.status === "skipped") {
    return { elapsedMs: 0, status: "skipped" };
  }
  if (elapsedMs < run.end) {
    return {
      elapsedMs: elapsedMs - run.start,
      status: node.kind === "approval" ? "waiting" : "running",
    };
  }
  return { elapsedMs: run.end - run.start, status: run.status };
};

export const getEdgeState = (
  id: string,
  schedule: WorkflowSchedule,
  phase: WorkflowRunPhase,
  elapsedMs: number
): WorkflowEdgeStatus => {
  const run = schedule.edges[id];
  if (!run || phase === "idle" || elapsedMs < run.start) {
    return "idle";
  }
  if (!run.taken) {
    return "skipped";
  }
  return elapsedMs < run.end ? "active" : "done";
};

export const formatDuration = (ms: number) => {
  if (ms < 60_000) {
    return `${(ms / 1000).toFixed(1)}s`;
  }
  if (ms < 3_600_000) {
    const minutes = Math.floor(ms / 60_000);
    const seconds = Math.floor((ms % 60_000) / 1000);
    return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
  }
  const hours = Math.floor(ms / 3_600_000);
  const minutes = Math.floor((ms % 3_600_000) / 60_000);
  return `${hours}h ${String(minutes).padStart(2, "0")}m`;
};

export const formatRate = (rate: number) => `${Math.round(rate * 100)}%`;

export const formatClock = (ms: number) => {
  const minutes = Math.floor(ms / 60_000);
  const seconds = Math.floor((ms % 60_000) / 1000);
  const tenths = Math.floor((ms % 1000) / 100);
  return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${tenths}`;
};

const branchLabel = (node: WorkflowNodeData) =>
  getOutputPorts(node).find((port) => port.id === node.branch)?.label ??
  node.branch;

export const getRunLog = (
  nodes: WorkflowNodeData[],
  schedule: WorkflowSchedule,
  phase: WorkflowRunPhase,
  elapsedMs: number
): WorkflowLogEntry[] => {
  if (phase === "idle") {
    return [];
  }
  const entries: WorkflowLogEntry[] = [];
  const byId = new Map(nodes.map((node) => [node.id, node]));
  for (const id of schedule.order) {
    const node = byId.get(id);
    const run = schedule.nodes[id];
    if (!(node && run) || elapsedMs < run.start) {
      continue;
    }
    const base = { nodeId: id, title: node.title };
    if (run.status === "skipped") {
      entries.push({
        ...base,
        at: run.start,
        id: `${id}:skip`,
        level: "skip",
        message: "Skipped, no earlier step led here",
      });
      continue;
    }
    entries.push({
      ...base,
      at: run.start,
      id: `${id}:start`,
      level: "info",
      message: node.kind === "approval" ? "Waiting for approval" : "Started",
    });
    if (elapsedMs < run.end) {
      continue;
    }
    const took = run.end - run.start;
    let message = `Completed in ${formatDuration(took)}`;
    if (run.status === "error") {
      message = `Failed after ${formatDuration(took)}`;
    } else if (node.branch !== undefined) {
      message = `Took the "${branchLabel(node)}" branch`;
    }
    entries.push({
      ...base,
      at: run.end,
      id: `${id}:end`,
      level: run.status === "error" ? "error" : "success",
      message,
    });
  }
  return entries.sort((a, b) => a.at - b.at);
};

export const getEdgePath = (
  from: WorkflowPoint,
  to: WorkflowPoint,
  radius = WORKFLOW_EDGE_RADIUS
) => {
  const dx = to.x - from.x;
  const dy = to.y - from.y;
  const midX = from.x + dx / 2;
  const r = Math.min(radius, Math.abs(dy) / 2, Math.abs(dx) / 2);
  if (r < 0.5) {
    return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;
  }
  const sx = Math.sign(dx);
  const sy = Math.sign(dy);
  const sweep = sx * sy > 0 ? 1 : 0;
  return [
    `M ${from.x} ${from.y}`,
    `L ${midX - sx * r} ${from.y}`,
    `A ${r} ${r} 0 0 ${sweep} ${midX} ${from.y + sy * r}`,
    `L ${midX} ${to.y - sy * r}`,
    `A ${r} ${r} 0 0 ${1 - sweep} ${midX + sx * r} ${to.y}`,
    `L ${to.x} ${to.y}`,
  ].join(" ");
};

export const getFlowEdgePath = (
  source: WorkflowAnchor,
  target: WorkflowAnchor,
  reverse = false
): [string, number, number] => {
  const [from, to] = reverse ? [target, source] : [source, target];
  if (target.x > source.x) {
    return [
      getEdgePath(from, to),
      (source.x + target.x) / 2,
      (source.y + target.y) / 2,
    ];
  }
  const [path, labelX, labelY] = getSmoothStepPath({
    ...WORKFLOW_EDGE_STEP_OPTIONS,
    sourcePosition: from.position,
    sourceX: from.x,
    sourceY: from.y,
    targetPosition: to.position,
    targetX: to.x,
    targetY: to.y,
  });
  return [path, labelX, labelY];
};

export const createNodeFromBlock = (
  block: WorkflowBlock,
  position: WorkflowPoint
): WorkflowNodeData => ({
  branch:
    block.outputs && block.outputs.length > 1 ? block.outputs[0].id : undefined,
  description: block.description,
  durationMs: block.durationMs,
  id: `${block.id}-${Date.now().toString(36)}`,
  inputs: block.inputs,
  kind: block.kind,
  logo: block.logo,
  model: block.model,
  outputs: block.outputs,
  position,
  title: block.title,
});
```

### `registry/notra/workflow/types/workflow.ts`

```ts
import type { Edge, Node, Position } from "@xyflow/react";
import type { ComponentProps, ComponentType, ReactNode, SVGProps } from "react";

export type WorkflowNodeKind =
  | "trigger"
  | "task"
  | "ai"
  | "integration"
  | "condition"
  | "switch"
  | "merge"
  | "loop"
  | "delay"
  | "approval"
  | "email"
  | "output";

export type WorkflowNodeStatus =
  | "idle"
  | "queued"
  | "running"
  | "waiting"
  | "success"
  | "error"
  | "skipped";

export type WorkflowEdgeStatus = "idle" | "active" | "done" | "skipped";

export type WorkflowRunPhase = "idle" | "running" | "done";

export type WorkflowLogoName =
  | "openai"
  | "anthropic"
  | "claude"
  | "gemini"
  | "perplexity"
  | "mistral"
  | "deepseek"
  | "grok"
  | "browserbase"
  | "context-dev"
  | "firecrawl"
  | "resend"
  | "slack"
  | "gmail"
  | "notion"
  | "google"
  | "google-sheets"
  | "webflow"
  | "wordpress"
  | "linear"
  | "github"
  | "ahrefs"
  | "posthog";

export type WorkflowLogLevel = "info" | "success" | "error" | "skip";

export type WorkflowIcon = ComponentType<SVGProps<SVGSVGElement>>;

export interface WorkflowPoint {
  x: number;
  y: number;
}

export interface WorkflowAnchor extends WorkflowPoint {
  position: Position;
}

export interface WorkflowPort {
  id: string;
  label?: string;
  hint?: string;
  rate?: number;
}

export interface WorkflowRule {
  subject: string;
  operator: string;
  value: string;
}

export interface WorkflowField {
  label: string;
  value: string;
}

export interface WorkflowNodeData {
  id: string;
  kind: WorkflowNodeKind;
  title: string;
  description?: string;
  position: WorkflowPoint;
  inputs?: WorkflowPort[];
  outputs?: WorkflowPort[];
  logo?: WorkflowLogoName;
  model?: string;
  durationMs?: number;
  branch?: string;
  rule?: WorkflowRule;
  fail?: boolean;
  fields?: WorkflowField[];
  output?: string;
}

export interface WorkflowEdgeData {
  id: string;
  source: string;
  sourcePort?: string;
  target: string;
  targetPort?: string;
  label?: string;
  animated?: boolean;
}

export interface WorkflowGraph {
  edges: WorkflowEdgeData[];
  nodes: WorkflowNodeData[];
}

export interface WorkflowTemplate extends WorkflowGraph {
  id: string;
  title: string;
  description: string;
  category: string;
}

export interface WorkflowKindConfig {
  icon: WorkflowIcon;
  label: string;
  tileClassName: string;
}

export interface WorkflowStatusConfig {
  className: string;
  label: string;
  ledClassName: string;
}

export interface WorkflowLogoConfig {
  label: string;
  logo: WorkflowIcon;
  tile?: boolean;
}

export interface WorkflowModel {
  id: string;
  label: string;
  logo: WorkflowLogoName;
  provider: string;
}

export interface WorkflowBlock {
  id: string;
  title: string;
  description: string;
  group: string;
  kind: WorkflowNodeKind;
  logo?: WorkflowLogoName;
  model?: string;
  inputs?: WorkflowPort[];
  outputs?: WorkflowPort[];
  durationMs?: number;
}

export interface WorkflowNodeRun {
  start: number;
  end: number;
  status: "success" | "error" | "skipped";
}

export interface WorkflowEdgeRun {
  start: number;
  end: number;
  taken: boolean;
}

export interface WorkflowSchedule {
  edges: Record<string, WorkflowEdgeRun>;
  nodes: Record<string, WorkflowNodeRun>;
  order: string[];
  totalMs: number;
}

export interface WorkflowNodeState {
  elapsedMs: number;
  status: WorkflowNodeStatus;
}

export interface WorkflowLogEntry {
  at: number;
  id: string;
  level: WorkflowLogLevel;
  message: string;
  nodeId: string;
  title: string;
}

export interface WorkflowRunSummary {
  id: string;
  durationMs: number;
  startedAt: string;
  status: "success" | "error" | "running";
  trigger: string;
  steps: number;
}

export interface WorkflowRun {
  edgeState: (id: string) => WorkflowEdgeStatus;
  elapsedMs: number;
  nodeState: (id: string) => WorkflowNodeState;
  phase: WorkflowRunPhase;
  play: () => void;
  schedule: WorkflowSchedule;
  stop: () => void;
  totalMs: number;
}

export type WorkflowLogoProps = Omit<SVGProps<SVGSVGElement>, "name"> & {
  name: WorkflowLogoName;
};

export interface WorkflowFlowNodeData extends Record<string, unknown> {
  node: WorkflowNodeData;
  state?: WorkflowNodeState;
}

export interface WorkflowFlowEdgeData extends Record<string, unknown> {
  animated?: boolean;
  reverse?: boolean;
  label?: string;
  status: WorkflowEdgeStatus;
}

export type WorkflowFlowNode = Node<WorkflowFlowNodeData, "workflow">;

export type WorkflowFlowEdge = Edge<WorkflowFlowEdgeData, "workflow">;

export type WorkflowCanvasBackgroundVariant =
  | "dots"
  | "lines"
  | "cross"
  | "grid"
  | "none";

export interface WorkflowCanvasBackgroundProps {
  gap?: number;
  variant?: WorkflowCanvasBackgroundVariant;
}

export type WorkflowNodeSizes = Record<
  string,
  { height: number; width: number }
>;

export interface WorkflowPortListProps {
  ports: WorkflowPort[];
  title: string;
}

export interface WorkflowControlsProps {
  toolbar?: ReactNode;
}

export interface WorkflowBannerConfig {
  className: string;
  label: string;
  message: string;
}

export interface WorkflowStatusGlyphProps {
  status: WorkflowNodeStatus;
}

export interface WorkflowTemplatePreviewProps {
  template: WorkflowTemplate;
}

export interface WorkflowCanvasProps {
  className?: string;
  focusId?: string;
  background?: WorkflowCanvasBackgroundVariant;
  gap?: number;
  nodes: WorkflowNodeData[];
  edges: WorkflowEdgeData[];
  nodeState?: (id: string) => WorkflowNodeState;
  edgeState?: (id: string) => WorkflowEdgeStatus;
  selectedId?: string;
  controls?: boolean;
  minimap?: boolean;
  toolbar?: ReactNode;
  minZoom?: number;
  maxZoom?: number;
  onSelectNode?: (id: string) => void;
  onNodeMove?: (id: string, position: WorkflowPoint) => void;
  onNodeRemove?: (id: string) => void;
  onConnect?: (edge: Omit<WorkflowEdgeData, "id">) => void;
  onDropBlock?: (blockId: string, position: WorkflowPoint) => void;
}

export type WorkflowNodeCardProps = ComponentProps<"div"> & {
  node: WorkflowNodeData;
  state?: WorkflowNodeState;
  selected?: boolean;
  connectable?: boolean;
  children?: ReactNode;
};

export interface WorkflowHandleProps {
  active?: boolean;
  className?: string;
  connectable?: boolean;
  portId: string;
  type: "source" | "target";
}

export type WorkflowEdgeProps = ComponentProps<"g"> & {
  path: string;
  reverse?: boolean;
  status?: WorkflowEdgeStatus;
  animated?: boolean;
};

export type WorkflowTimerProps = ComponentProps<"span"> & {
  ms: number;
  running?: boolean;
};

export type WorkflowStatusIndicatorProps = ComponentProps<"span"> & {
  status: WorkflowNodeStatus;
};

export type WorkflowStatusBadgeProps = ComponentProps<"span"> & {
  status: WorkflowNodeStatus;
};

export type WorkflowStatusPillProps = ComponentProps<"span"> & {
  status: WorkflowNodeStatus;
};

export type WorkflowDeterminismBadgeProps = ComponentProps<"span"> & {
  deterministic: boolean;
};

export type WorkflowRunButtonProps = Omit<
  ComponentProps<"button">,
  "children"
> & {
  phase: WorkflowRunPhase;
  progress?: number;
  onPlay?: () => void;
  onStop?: () => void;
};

export type WorkflowNodePaletteProps = ComponentProps<"div"> & {
  blocks: WorkflowBlock[];
  onAdd?: (block: WorkflowBlock) => void;
};

export interface WorkflowNodeBranchProps {
  connectable?: boolean;
  node: WorkflowNodeData;
  port: WorkflowPort;
  status: WorkflowNodeStatus;
}

export interface WorkflowNodeBannerProps {
  status: WorkflowNodeStatus;
}

export interface WorkflowNodeSummaryProps {
  children?: ReactNode;
  node: WorkflowNodeData;
}

export interface WorkflowNodeEngineProps {
  node: WorkflowNodeData;
}

export interface WorkflowNodeFooterProps {
  node: WorkflowNodeData;
  state?: WorkflowNodeState;
}

export interface WorkflowBranchSelectProps {
  node: WorkflowNodeData;
  outputs: WorkflowPort[];
  onBranchChange: (id: string, branch: string) => void;
}

export type WorkflowNodeSettingsProps = Pick<
  WorkflowNodeSheetProps,
  "onBranchChange" | "onModelChange"
> & {
  node: WorkflowNodeData;
};

export type WorkflowNodeSheetProps = WorkflowOverlayProps & {
  node?: WorkflowNodeData;
  state?: WorkflowNodeState;
  log?: WorkflowLogEntry[];
  onDelete?: (id: string) => void;
  onModelChange?: (id: string, model: string) => void;
  onBranchChange?: (id: string, branch: string) => void;
};

export type WorkflowRunLogProps = ComponentProps<"div"> & {
  entries: WorkflowLogEntry[];
  emptyLabel?: string;
};

export type WorkflowRunTimelineProps = ComponentProps<"div"> & {
  nodes: WorkflowNodeData[];
  schedule: WorkflowSchedule;
  elapsedMs: number;
  phase: WorkflowRunPhase;
};

export type WorkflowRunHistoryProps = Omit<
  ComponentProps<"div">,
  "onSelect"
> & {
  runs: WorkflowRunSummary[];
  selectedId?: string;
  onSelect?: (id: string) => void;
};

export interface WorkflowModelSelectProps {
  className?: string;
  defaultValue?: string;
  disabled?: boolean;
  id?: string;
  models?: WorkflowModel[];
  onValueChange?: (value: string) => void;
  value?: string;
}

export type WorkflowTemplateCardProps = ComponentProps<"div"> & {
  template: WorkflowTemplate;
  onUse?: (template: WorkflowTemplate) => void;
};

export type WorkflowTemplateDialogProps = WorkflowOverlayProps & {
  template?: WorkflowTemplate;
  onUse?: (template: WorkflowTemplate) => void;
};

export interface WorkflowBlockIconProps {
  block: Pick<WorkflowBlock, "kind" | "logo" | "model">;
  className?: string;
}

export interface WorkflowOverlayProps {
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  open?: boolean;
}

export interface WorkflowCanvasBackgroundOption {
  label: string;
  value: WorkflowCanvasBackgroundVariant;
}

export interface WorkflowNodeDemo {
  node: WorkflowNodeData;
  state: WorkflowNodeState;
}

export interface WorkflowEdgeDemo {
  animated?: boolean;
  label: string;
  status: WorkflowEdgeStatus;
}
```
