Node
A step card with a kind icon or brand logo, a status light, handles, a model chip and a timer.
import { WorkflowNodeCard } from "../components/workflow-node";
import type { WorkflowNodeDemo } from "../types/workflow";
const EXAMPLES: WorkflowNodeDemo[] = [
{
node: {
description: "Every Monday at 09:00",
id: "trigger",
kind: "trigger",
position: { x: 0, y: 0 },
title: "Weekly schedule",
},
state: { elapsedMs: 0, status: "idle" },
},
{
node: {
description: "Is this a listicle we fit in?",
id: "classify",
kind: "ai",
model: "claude-sonnet-5-5",
position: { x: 0, y: 0 },
title: "Classify fit",
},
state: { elapsedMs: 1400, status: "running" },
},
{
node: {
description: "Searches 12 best-of prompts.",
id: "search",
kind: "integration",
logo: "browserbase",
position: { x: 0, y: 0 },
title: "Search listicles",
},
state: { elapsedMs: 1800, status: "success" },
},
{
node: {
branch: "no",
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: 0, y: 0 },
rule: { operator: "mentions", subject: "Listicle", value: "Notra" },
title: "Are we listed?",
},
state: { elapsedMs: 400, status: "success" },
},
{
node: {
description: "Waits for someone on the team.",
id: "review",
kind: "approval",
position: { x: 0, y: 0 },
title: "Review draft",
},
state: { elapsedMs: 3200, status: "waiting" },
},
{
node: {
description: "From your outreach inbox",
id: "send",
kind: "email",
logo: "resend",
position: { x: 0, y: 0 },
title: "Send outreach",
},
state: { elapsedMs: 600, status: "error" },
},
];
export default function WorkflowNodeExample() {
return (
<div className="flex w-full max-w-3xl flex-wrap items-start justify-center gap-6 px-3">
{EXAMPLES.map(({ node, state }) => (
<WorkflowNodeCard key={node.id} node={node} state={state} />
))}
</div>
);
}
$ bunx --bun shadcn@latest add @notra/workflow-nodeWorkflowNodeCard is the step the canvas draws for every node, built on the shell card. The inner card shows the step’s kind, title, description and a status light that glows while the step runs. The footer on the frame holds chips for the integration or model and how long the step takes. AI steps add a dice icon to the model chip to mark them as non-deterministic, and failed or waiting steps get a banner above the card.
Steps without labeled ports get one input and one output handle in the header. Give inputs labels to stack a row per input, which is how a merge takes several inputs. Labeled outputs become branches in a rail under the card, which is how an if/else or a decision routes the run. Each branch can say what happens next with hint and how often it’s taken with rate, and the branch a run takes lights up.
Give an if/else a rule to spell out what it checks in plain words, like { subject: "Listicle", operator: "mentions", value: "Notra" }.
| Prop | Default | Description |
|---|---|---|
node |
The step: id, kind, title, position and optional description, logo, model, rule, inputs, outputs, branch and durationMs. |
|
state |
{ status, elapsedMs }. Running and waiting steps tick their timer. |
|
selected |
Draws the selection ring. | |
connectable |
Renders React Flow handles. Leave it off to use the card outside a canvas, like in a list or a preview. |
WorkflowFlowNode is the React Flow node type that wraps the card with connectable on. Register it as a nodeType to use workflow steps in your own ReactFlow.
kind is one of trigger, task, ai, integration, condition, switch, merge, loop, delay, approval, email or output.
Ports
"use client";
import { WorkflowCanvas } from "../components/workflow-canvas";
import { useWorkflowGraph } from "../hooks/use-workflow-graph";
import type { WorkflowGraph } from "../types/workflow";
const GRAPH: WorkflowGraph = {
edges: [
{ id: "e1", source: "traffic", target: "summary", targetPort: "traffic" },
{
id: "e2",
source: "visibility",
target: "summary",
targetPort: "visibility",
},
{ id: "e3", source: "summary", target: "route" },
{
id: "e4",
label: "≥ 10%",
source: "route",
sourcePort: "up",
target: "slack",
},
{ id: "e5", source: "route", sourcePort: "flat", target: "sheet" },
{
id: "e6",
label: "≤ -10%",
source: "route",
sourcePort: "down",
target: "linear",
},
],
nodes: [
{
id: "traffic",
kind: "integration",
logo: "posthog",
position: { x: 32, y: 72 },
title: "AI referral traffic",
},
{
id: "visibility",
kind: "task",
position: { x: 32, y: 288 },
title: "Visibility report",
},
{
id: "summary",
inputs: [
{ id: "traffic", label: "Traffic" },
{ id: "visibility", label: "Visibility" },
],
kind: "ai",
model: "claude-haiku-5-5",
position: { x: 352, y: 144 },
title: "Write digest",
},
{
id: "route",
kind: "switch",
outputs: [
{ hint: "Share the win", id: "up", label: "Visibility up" },
{ hint: "Add to the sheet", id: "flat", label: "No change" },
{ hint: "Open a Linear issue", id: "down", label: "Visibility down" },
],
position: { x: 680, y: 144 },
rule: {
operator: "compared to",
subject: "AI visibility",
value: "last week",
},
title: "Week over week",
},
{
id: "slack",
kind: "integration",
logo: "slack",
position: { x: 1056, y: 96.5 },
title: "Celebrate in #geo",
},
{
id: "sheet",
kind: "integration",
logo: "google-sheets",
position: { x: 1056, y: 272.5 },
title: "Log to sheet",
},
{
id: "linear",
kind: "integration",
logo: "linear",
position: { x: 1056, y: 432.5 },
title: "Open an issue",
},
],
};
export default function WorkflowPortsExample() {
const graph = useWorkflowGraph(GRAPH);
return (
<WorkflowCanvas
className="h-80 w-full"
edges={graph.edges}
nodes={graph.nodes}
onConnect={graph.connect}
onNodeMove={graph.moveNode}
/>
);
}
{
id: "route",
kind: "switch",
title: "Week over week",
outputs: [
{ id: "up", label: "Visibility up" },
{ id: "flat", label: "No change" },
{ id: "down", label: "Visibility down" },
],
}
Used in
GEO Workflow Builder
A workflow builder with a block palette, a playable canvas, a run log and a node sheet.