Create Agent
A two-step wizard card for naming an agent, picking its icon, and selecting which event sources activate it.
New agent
Name your agent
Give it a name, instructions, and an icon.
- Require approvalHuman must approve before any write action
- Read-only by defaultCannot push code or send messages directly
- Limit to 1 action per triggerPrevents runaway loops on rapid events
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import {
Antenna,
AtSign,
Bot,
Brain,
Check,
ChevronRight,
Code2,
Compass,
Cpu,
Database,
Eye,
FlaskConical,
GitBranch,
Globe,
Layers,
Lightbulb,
Plus,
Rocket,
Search,
Server,
Shield,
ShieldAlert,
ShieldOff,
Sparkles,
Star,
Terminal,
Webhook,
Wrench,
X,
Zap,
} from "lucide-react";
import { cn } from "@/lib/utils";
// ── Trigger sources ──────────────────────────────────────────────────────────
interface TriggerField {
id: string;
label: string;
type: "text" | "select";
placeholder?: string;
options?: { value: string; label: string }[];
}
export const TRIGGER_SOURCES = [
{
id: "github" as const,
label: "GitHub",
icon: GitBranch,
description: "Issues, pull requests, and labels",
fields: [
{
id: "repo",
label: "Repository",
type: "text" as const,
placeholder: "e.g. carlosmarch/ai-patterns",
},
{
id: "event",
label: "Event",
type: "select" as const,
options: [
{ value: "issue-labeled", label: "Issue labeled" },
{ value: "pr-opened", label: "PR opened" },
{ value: "pr-merged", label: "PR merged" },
],
},
] satisfies TriggerField[],
},
{
id: "slack" as const,
label: "Slack",
icon: AtSign,
description: "Mentions, messages, and reactions",
fields: [
{
id: "channel",
label: "Channel",
type: "text" as const,
placeholder: "e.g. #design-requests",
},
{
id: "event",
label: "Trigger on",
type: "select" as const,
options: [
{ value: "mention", label: "Agent mentioned" },
{ value: "keyword", label: "Keyword match" },
],
},
] satisfies TriggerField[],
},
{
id: "figma" as const,
label: "Figma",
icon: Layers,
description: "Frame status changes and comments",
fields: [
{
id: "scope",
label: "Files",
type: "select" as const,
options: [
{ value: "any", label: "Any Figma file" },
{ value: "specific", label: "Specific file URL" },
],
},
{
id: "event",
label: "Event",
type: "select" as const,
options: [
{ value: "ready-for-dev", label: "Frame ready for dev" },
{ value: "comment", label: "New comment" },
],
},
] satisfies TriggerField[],
},
] as const;
export type TriggerSourceId = (typeof TRIGGER_SOURCES)[number]["id"];
// ── Agent icons ───────────────────────────────────────────────────────────────
export const AGENT_ICONS = [
{ id: "bot" as const, icon: Bot, label: "Bot" },
{ id: "sparkles" as const, icon: Sparkles, label: "Sparkles" },
{ id: "zap" as const, icon: Zap, label: "Zap" },
{ id: "brain" as const, icon: Brain, label: "Brain" },
{ id: "rocket" as const, icon: Rocket, label: "Rocket" },
] as const;
export const EXTRA_AGENT_ICONS = [
{ id: "cpu" as const, icon: Cpu, label: "CPU" },
{ id: "globe" as const, icon: Globe, label: "Globe" },
{ id: "terminal" as const, icon: Terminal, label: "Terminal" },
{ id: "code2" as const, icon: Code2, label: "Code" },
{ id: "wrench" as const, icon: Wrench, label: "Wrench" },
{ id: "search" as const, icon: Search, label: "Search" },
{ id: "eye" as const, icon: Eye, label: "Eye" },
{ id: "star" as const, icon: Star, label: "Star" },
{ id: "database" as const, icon: Database, label: "Database" },
{ id: "server" as const, icon: Server, label: "Server" },
{ id: "lightbulb" as const, icon: Lightbulb, label: "Lightbulb" },
{ id: "compass" as const, icon: Compass, label: "Compass" },
{ id: "flask" as const, icon: FlaskConical, label: "Flask" },
{ id: "antenna" as const, icon: Antenna, label: "Antenna" },
] as const;
export const ALL_AGENT_ICONS = [...AGENT_ICONS, ...EXTRA_AGENT_ICONS];
export type AgentIconId = (typeof ALL_AGENT_ICONS)[number]["id"];
// ── Guardrails ────────────────────────────────────────────────────────────────
export interface Guardrail {
id: string;
label: string;
description: string;
icon: React.ComponentType<{ className?: string }>;
enabled: boolean;
}
export const DEFAULT_GUARDRAILS: Guardrail[] = [
{
id: "approval-required",
label: "Require approval",
description: "Human must approve before any write action",
icon: ShieldAlert,
enabled: true,
},
{
id: "read-only",
label: "Read-only by default",
description: "Cannot push code or send messages directly",
icon: ShieldOff,
enabled: true,
},
{
id: "rate-limit",
label: "Limit to 1 action per trigger",
description: "Prevents runaway loops on rapid events",
icon: Shield,
enabled: false,
},
];
// ── Payload ───────────────────────────────────────────────────────────────────
export interface SelectedTrigger {
sourceId: TriggerSourceId;
config: Record<string, string>;
}
export interface CustomTrigger {
id: string;
name: string;
event: string;
}
export interface CreateAgentPayload {
name: string;
instructions: string;
iconId: AgentIconId;
guardrails: { id: string; enabled: boolean }[];
triggers: SelectedTrigger[];
customTriggers: CustomTrigger[];
}
// ── Props ─────────────────────────────────────────────────────────────────────
export interface CreateAgentProps {
onCreateAgent?: (payload: CreateAgentPayload) => void;
className?: string;
}
// ── Component ─────────────────────────────────────────────────────────────────
export function CreateAgent({ onCreateAgent, className }: CreateAgentProps) {
const [step, setStep] = React.useState<0 | 1>(0);
const [direction, setDirection] = React.useState<1 | -1>(1);
const [name, setName] = React.useState("");
const [instructions, setInstructions] = React.useState("");
const [iconId, setIconId] = React.useState<AgentIconId>("bot");
const [selectedSources, setSelectedSources] = React.useState<TriggerSourceId[]>([]);
const [triggerConfigs, setTriggerConfigs] = React.useState<
Partial<Record<TriggerSourceId, Record<string, string>>>
>({});
const [customTriggers, setCustomTriggers] = React.useState<CustomTrigger[]>([]);
const [showMoreIcons, setShowMoreIcons] = React.useState(false);
const [guardrailStates, setGuardrailStates] = React.useState<Record<string, boolean>>(
Object.fromEntries(DEFAULT_GUARDRAILS.map((g) => [g.id, g.enabled]))
);
const selectedIcon = ALL_AGENT_ICONS.find((i) => i.id === iconId)!;
const SelectedIconComp = selectedIcon.icon;
function goToStep1() {
setDirection(1);
setStep(1);
}
function goToStep0() {
setDirection(-1);
setStep(0);
}
function toggleSource(id: TriggerSourceId) {
setSelectedSources((prev) =>
prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id]
);
}
function setTriggerField(sourceId: TriggerSourceId, fieldId: string, value: string) {
setTriggerConfigs((prev) => ({
...prev,
[sourceId]: { ...prev[sourceId], [fieldId]: value },
}));
}
function addCustomTrigger() {
setCustomTriggers((prev) => [
...prev,
{ id: `custom-${Date.now()}`, name: "", event: "" },
]);
}
function updateCustomTrigger(id: string, field: keyof Omit<CustomTrigger, "id">, value: string) {
setCustomTriggers((prev) =>
prev.map((t) => (t.id === id ? { ...t, [field]: value } : t))
);
}
function removeCustomTrigger(id: string) {
setCustomTriggers((prev) => prev.filter((t) => t.id !== id));
}
function toggleGuardrail(id: string) {
setGuardrailStates((prev) => ({ ...prev, [id]: !prev[id] }));
}
function buildPayload(): CreateAgentPayload {
return {
name: name.trim() || "Unnamed Agent",
instructions: instructions.trim(),
iconId,
guardrails: DEFAULT_GUARDRAILS.map((g) => ({ id: g.id, enabled: guardrailStates[g.id] ?? g.enabled })),
triggers: selectedSources.map((sourceId) => ({
sourceId,
config: triggerConfigs[sourceId] ?? {},
})),
customTriggers,
};
}
const variants = {
enter: (dir: number) => ({ x: dir * 16, opacity: 0 }),
center: { x: 0, opacity: 1 },
exit: (dir: number) => ({ x: dir * -16, opacity: 0 }),
};
return (
<div suppressHydrationWarning className={cn("w-full max-w-xs overflow-hidden rounded-2xl border bg-card", className)}>
{/* Header preview */}
<div className="flex items-center justify-between gap-3 border-b px-4 py-3">
<div className="flex min-w-0 items-center gap-2">
<span className="flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
<SelectedIconComp className="size-3.5" />
</span>
<p className="truncate text-sm font-semibold text-foreground">
{name.trim() || "New agent"}
</p>
</div>
<div className="flex shrink-0 items-center gap-1" aria-hidden>
{[0, 1].map((i) => (
<span
key={i}
className={cn(
"block h-1.5 rounded-full transition-all duration-200",
step === i ? "w-4 bg-foreground" : "w-1.5 bg-muted-foreground/30"
)}
/>
))}
</div>
</div>
{/* Steps */}
<div className="relative overflow-hidden">
<AnimatePresence initial={false} custom={direction} mode="wait">
{step === 0 ? (
<motion.div
key="step-identity"
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.15, ease: "easeInOut" }}
className="px-4 pb-4 pt-3"
>
<p className="text-sm font-semibold">Name your agent</p>
<p className="mt-0.5 text-xs text-muted-foreground">
Give it a name, instructions, and an icon.
</p>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. AI-Patterns"
aria-label="Agent name"
className="mt-3 w-full rounded-lg border bg-background px-3 py-2 text-sm outline-none ring-ring placeholder:text-muted-foreground/50 focus-visible:ring-2"
/>
<textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="e.g. When a GitHub issue is labeled pattern-request in carlosmarch/ai-patterns, triage it and suggest a matching component."
aria-label="Agent instructions"
rows={3}
className="mt-2 w-full resize-none rounded-lg border bg-background px-3 py-2 text-sm outline-none ring-ring placeholder:text-muted-foreground/50 focus-visible:ring-2"
/>
<ul className="mt-3 divide-y rounded-xl border">
{DEFAULT_GUARDRAILS.map(({ id, label, description, icon: Icon }) => {
const enabled = guardrailStates[id] ?? false;
return (
<li key={id} className="flex items-center gap-3 px-3 py-2.5">
<span className={cn("flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground")}>
<Icon className="size-3.5" />
</span>
<span className="min-w-0 flex-1">
<span className="block text-xs font-medium">{label}</span>
<span className="block text-[11px] text-muted-foreground">{description}</span>
</span>
<button
type="button"
role="switch"
aria-checked={enabled}
aria-label={label}
onClick={() => toggleGuardrail(id)}
className={cn(
"relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
enabled ? "bg-foreground" : "bg-muted"
)}
>
<motion.span
aria-hidden
className="pointer-events-none inline-block h-4 w-4 rounded-full bg-background shadow-sm"
animate={{ x: enabled ? 16 : 0 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
</button>
</li>
);
})}
</ul>
<div className="mt-3 space-y-1.5">
<div className="flex items-center gap-1.5">
{AGENT_ICONS.map(({ id, icon: Icon, label }) => (
<button
key={id}
type="button"
aria-label={label}
aria-pressed={iconId === id}
onClick={() => setIconId(id)}
className={cn(
"flex size-8 items-center justify-center rounded-lg transition-colors",
iconId === id
? "bg-foreground text-background"
: "bg-muted text-muted-foreground hover:text-foreground"
)}
>
<Icon className="size-4" />
</button>
))}
<button
type="button"
aria-label={showMoreIcons ? "Show fewer icons" : "Show more icons"}
aria-expanded={showMoreIcons}
onClick={() => setShowMoreIcons((v) => !v)}
className={cn(
"flex size-8 items-center justify-center rounded-lg transition-colors",
showMoreIcons
? "bg-muted text-foreground"
: "bg-muted text-muted-foreground hover:text-foreground"
)}
>
<Plus className={cn("size-4 transition-transform duration-150", showMoreIcons && "rotate-45")} />
</button>
</div>
<AnimatePresence initial={false}>
{showMoreIcons && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.15, ease: "easeInOut" }}
className="overflow-hidden"
>
<div className="flex flex-wrap gap-1.5 pt-0.5">
{EXTRA_AGENT_ICONS.map(({ id, icon: Icon, label }) => (
<button
key={id}
type="button"
aria-label={label}
aria-pressed={iconId === id}
onClick={() => { setIconId(id); setShowMoreIcons(false); }}
className={cn(
"flex size-8 items-center justify-center rounded-lg transition-colors",
iconId === id
? "bg-foreground text-background"
: "bg-muted text-muted-foreground hover:text-foreground"
)}
>
<Icon className="size-4" />
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<button
type="button"
onClick={goToStep1}
className="mt-4 flex w-full items-center justify-center gap-1.5 rounded-lg bg-foreground px-4 py-2 text-sm font-medium text-background transition-opacity hover:opacity-90"
>
Choose triggers
<ChevronRight className="size-3.5" />
</button>
<button
type="button"
onClick={() => onCreateAgent?.(buildPayload())}
className="mt-2 w-full text-center text-xs text-muted-foreground transition-colors hover:text-foreground"
>
Skip triggers
</button>
</motion.div>
) : (
<motion.div
key="step-triggers"
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.15, ease: "easeInOut" }}
className="px-4 pb-4 pt-3"
>
<p className="text-sm font-semibold">Choose triggers</p>
<p className="mt-0.5 text-xs text-muted-foreground">
Select the sources that activate this agent.
</p>
<ul className="mt-3 flex flex-col gap-2">
{TRIGGER_SOURCES.map(({ id, label, icon: Icon, description, fields }) => {
const isSelected = selectedSources.includes(id);
const config = triggerConfigs[id] ?? {};
return (
<li key={id} className={cn("rounded-xl border transition-colors", isSelected ? "border-foreground/20 bg-muted/40" : "")}>
{/* Toggle row */}
<button
type="button"
aria-pressed={isSelected}
onClick={() => toggleSource(id)}
className="flex w-full items-center gap-3 px-3 py-2.5 text-left"
>
<span className="flex size-7 shrink-0 items-center justify-center rounded-md border bg-background text-muted-foreground">
<Icon className="size-3.5" />
</span>
<span className="min-w-0 flex-1">
<span className="block text-xs font-medium">{label}</span>
<span className="block text-[11px] text-muted-foreground">{description}</span>
</span>
<span
className={cn(
"flex size-4 shrink-0 items-center justify-center rounded-full border transition-colors",
isSelected
? "border-foreground bg-foreground text-background"
: "border-muted-foreground/30"
)}
>
{isSelected && <Check className="size-2.5" strokeWidth={3} />}
</span>
</button>
{/* Expanded config */}
<AnimatePresence initial={false}>
{isSelected && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.15, ease: "easeInOut" }}
className="overflow-hidden"
>
<div className="flex flex-col gap-2 border-t px-3 pb-3 pt-2.5">
{fields.map((field) => (
<div key={field.id} className="flex flex-col gap-1">
<label className="text-[11px] font-medium text-muted-foreground">
{field.label}
</label>
{field.type === "text" ? (
<input
type="text"
value={config[field.id] ?? ""}
onChange={(e) => setTriggerField(id, field.id, e.target.value)}
placeholder={field.placeholder}
className="w-full rounded-md border bg-background px-2.5 py-1.5 text-xs outline-none ring-ring placeholder:text-muted-foreground/40 focus-visible:ring-2"
/>
) : (
<select
value={config[field.id] ?? field.options?.[0]?.value ?? ""}
onChange={(e) => setTriggerField(id, field.id, e.target.value)}
className="w-full rounded-md border bg-background px-2.5 py-1.5 text-xs outline-none ring-ring focus-visible:ring-2"
>
{field.options?.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
)}
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</li>
);
})}
</ul>
{/* Custom triggers */}
<AnimatePresence initial={false}>
{customTriggers.map((ct) => (
<motion.div
key={ct.id}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.15, ease: "easeInOut" }}
className="overflow-hidden"
>
<div className="mt-2 rounded-xl border border-foreground/20 bg-muted/40">
<div className="flex items-center gap-2 px-3 py-2.5">
<span className="flex size-7 shrink-0 items-center justify-center rounded-md border bg-background text-muted-foreground">
<Webhook className="size-3.5" />
</span>
<span className="flex-1 text-xs font-medium text-muted-foreground">
Custom trigger
</span>
<button
type="button"
aria-label="Remove custom trigger"
onClick={() => removeCustomTrigger(ct.id)}
className="flex size-5 items-center justify-center rounded text-muted-foreground/50 transition-colors hover:text-foreground"
>
<X className="size-3.5" />
</button>
</div>
<div className="flex flex-col gap-2 border-t px-3 pb-3 pt-2.5">
<div className="flex flex-col gap-1">
<label className="text-[11px] font-medium text-muted-foreground">
Name
</label>
<input
type="text"
value={ct.name}
onChange={(e) => updateCustomTrigger(ct.id, "name", e.target.value)}
placeholder="e.g. Webhook received"
className="w-full rounded-md border bg-background px-2.5 py-1.5 text-xs outline-none ring-ring placeholder:text-muted-foreground/40 focus-visible:ring-2"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-[11px] font-medium text-muted-foreground">
Event source
</label>
<input
type="text"
value={ct.event}
onChange={(e) => updateCustomTrigger(ct.id, "event", e.target.value)}
placeholder="e.g. POST /webhooks/my-agent"
className="w-full rounded-md border bg-background px-2.5 py-1.5 text-xs outline-none ring-ring placeholder:text-muted-foreground/40 focus-visible:ring-2"
/>
</div>
</div>
</div>
</motion.div>
))}
</AnimatePresence>
<button
type="button"
onClick={addCustomTrigger}
className="mt-2 flex w-full items-center gap-1.5 rounded-lg border border-dashed px-3 py-2 text-xs text-muted-foreground transition-colors hover:border-foreground/30 hover:text-foreground"
>
<Plus className="size-3.5" />
Add custom trigger
</button>
<div className="mt-3 flex gap-2">
<button
type="button"
onClick={goToStep0}
className="flex-1 rounded-lg border px-4 py-2 text-sm font-medium transition-colors hover:bg-muted"
>
Back
</button>
<button
type="button"
onClick={() => onCreateAgent?.(buildPayload())}
className="flex-1 rounded-lg bg-foreground px-4 py-2 text-sm font-medium text-background transition-opacity hover:opacity-90"
>
Create agent
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}
A UX spec for this pattern — written for agents implementing or reusing it, not the code.
# Create Agent
## Summary
A 2-step wizard card for creating a named, icon-identified agent and selecting which external event sources activate it. Step 1 captures identity (name and icon); Step 2 captures trigger sources. On completion it emits a typed payload that maps directly onto the Agent Triggers panel.
## When to use
- First-time agent creation where no agent exists yet.
- Onboarding flows that walk users through setting up their first automation.
- An agent catalog screen with an "Add agent" entry point that opens an inline or sheet-level wizard.
## When not to use
- Editing an existing agent's trigger sources — use the Agent Triggers pattern directly, which has per-trigger toggles and a "Finish setup" inline action.
- When only one trigger source is possible. Presenting a multi-select of one option adds unnecessary friction; skip to a single confirmation step instead.
- When agent creation is complex enough to require a full-page form (multiple steps beyond identity and triggers, required integrations, permissions review, etc.).
## Anatomy
1. **Header preview row** — always visible across both steps. Shows the currently selected icon in a rounded avatar, the live agent name (or "New agent" placeholder), and two step-progress dots.
2. **Step 1 — Identity** — a title/subtitle label pair, a text input for the agent name, a row of five icon-picker buttons (Bot, Sparkles, Zap, Brain, Rocket), and a full-width "Choose triggers →" CTA.
3. **Step 2 — Triggers** — a title/subtitle label pair, a `<ul>` of trigger source cards (each with a bordered icon square, a label and description, and a trailing checkmark circle), and a two-button footer with "Back" (outline) and "Create agent" (filled).
## Behavior
- The header preview row updates in real time: the name reflects every keystroke; the icon swaps immediately on selection. This gives users continuous feedback on how the finished agent will appear elsewhere in the product.
- Icon selection is immediate with no confirm step — clicking a picker button applies it at once.
- Trigger source cards are multi-select toggle buttons. The user may select zero, one, or all sources; there is no minimum enforcement in the component (enforce constraints in the consuming view if needed).
- Navigating from Step 1 to Step 2 slides content in from the right; navigating back slides from the left. AnimatePresence with `mode="wait"` ensures the outgoing step fully exits before the incoming step enters.
- "Create agent" is the terminal action. It fires `onCreateAgent` with a payload of `{ name, iconId, triggerSources }` and hands control entirely to the parent. The component itself has no post-creation state.
- The "Back" button does not reset Step 1 state — name and icon choices are preserved across forward/back navigation.
## Content guidelines
- Agent names should be short proper nouns or noun phrases that describe the agent's role, not its mechanism: "Pattern Bot", "Deploy Guard", "Design Review" — not "github-trigger-agent-v2".
- The placeholder text "e.g. Pattern Bot" demonstrates the expected format; replace it with a placeholder appropriate to the product domain.
- Trigger source labels name the platform (GitHub, Slack, Figma). Descriptions name the event type that fires the agent, not the agent's response: "Issues, pull requests, and labels" not "Monitors your repo and responds to issues".
- The "Create agent" button label should always say exactly that — avoid "Save", "Done", or "Finish", which imply the agent already exists.
## Accessibility
- The name input has a visible `<p>` label ("Name your agent") immediately above it and an `aria-label` attribute for programmatic association.
- Each icon-picker button has an `aria-label` naming the icon (e.g. "Sparkles") and `aria-pressed` reflecting selection state.
- Each trigger source card is a `<button>` with `aria-pressed` reflecting whether it is selected.
- The step-progress dots in the header are `aria-hidden` — they are a decorative visual indicator, not a navigation control.
- Step transitions use motion that respects `prefers-reduced-motion` via Motion's default behavior; the `duration: 0.15` transitions are brief enough that they cause minimal disruption even without the media-query guard.
## Related patterns
- **Agent Triggers** — Use it as well right after creation, since the Create Agent payload maps straight onto its title, agent and triggers props.
- **Setup Checklist** — Use it instead when onboarding needs several independent tasks done over time rather than one two-step wizard.
- **Invite Members** — Use it as a visual reference when both appear in one product, since they share the header preview, multi-step form and final action.
Do’s and don’ts
Do use it for
- First-time agent creation where no agent exists yet.
- Onboarding flows that walk users through setting up their first automation.
- An agent catalog screen with an "Add agent" entry point that opens an inline or sheet-level wizard.
Don’t use it for
- Editing an existing agent's trigger sources — use the Agent Triggers pattern directly, which has per-trigger toggles and a "Finish setup" inline action.
- When only one trigger source is possible. Presenting a multi-select of one option adds unnecessary friction; skip to a single confirmation step instead.
- When agent creation is complex enough to require a full-page form (multiple steps beyond identity and triggers, required integrations, permissions review, etc.).
Related patterns
Agent Triggers
Use it as well right after creation, since the Create Agent payload maps straight onto its title, agent and triggers props.
Setup Checklist
Use it instead when onboarding needs several independent tasks done over time rather than one two-step wizard.
Invite Members
Use it as a visual reference when both appear in one product, since they share the header preview, multi-step form and final action.