Agent Triggers
A configuration panel for listing and toggling the external events that activate an agent, with a manual run control.
AI-Patterns
When should this agent run?
Issue labeled pattern-request in carlosmarch/ai-patterns
When AI-Patterns is mentioned in #design-requests
Frame marked ready for dev in any Figma file
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { AtSign, GitBranch, GitPullRequest, Layers, Play, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
export interface AgentTrigger {
id: string;
icon: React.ComponentType<{ className?: string }>;
event: string;
context: string;
enabled: boolean;
needsSetup?: boolean;
}
export interface AgentAvatar {
name: string;
src?: string;
icon?: React.ComponentType<{ className?: string }>;
}
export interface AgentTriggersProps {
title?: string;
description?: string;
agent?: AgentAvatar;
triggers?: AgentTrigger[];
onRunAgent?: () => void;
onToggle?: (id: string, enabled: boolean) => void;
onAddTrigger?: (platformId?: string) => void;
onFinishSetup?: (id: string) => void;
className?: string;
}
const QUICK_ADD_PLATFORMS = [
{ id: "github", label: "GitHub", icon: GitBranch },
{ id: "slack", label: "Slack", icon: AtSign },
{ id: "figma", label: "Figma", icon: Layers },
] as const;
export const DEFAULT_TRIGGERS: AgentTrigger[] = [
{
id: "github-issue",
icon: GitPullRequest,
event: "Issue labeled pattern-request",
context: "in carlosmarch/ai-patterns",
enabled: true,
},
{
id: "slack-mention",
icon: AtSign,
event: "When AI-Patterns is mentioned",
context: "in #design-requests",
enabled: true,
},
{
id: "figma-ready",
icon: Layers,
event: "Frame marked ready for dev",
context: "in any Figma file",
enabled: false,
needsSetup: true,
},
];
function Avatar({ agent }: { agent: AgentAvatar }) {
return (
<span className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-muted-foreground">
{agent.src ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={agent.src} alt={agent.name} className="size-full object-cover" />
) : agent.icon ? (
<agent.icon className="size-4" />
) : (
<span className="text-xs font-semibold">
{agent.name.split(" ").map((w) => w[0]).slice(0, 2).join("").toUpperCase()}
</span>
)}
</span>
);
}
function Toggle({ enabled, onChange }: { enabled: boolean; onChange: (v: boolean) => void }) {
return (
<button
type="button"
role="switch"
aria-checked={enabled}
onClick={() => onChange(!enabled)}
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>
);
}
export function AgentTriggers({
title = "Triggers",
description = "When should this agent run?",
agent,
triggers: initialTriggers = DEFAULT_TRIGGERS,
onRunAgent,
onToggle,
onAddTrigger,
onFinishSetup,
className,
}: AgentTriggersProps) {
const [triggers, setTriggers] = React.useState(initialTriggers);
const [running, setRunning] = React.useState(false);
function handleToggle(id: string, value: boolean) {
setTriggers((prev) => prev.map((t) => (t.id === id ? { ...t, enabled: value } : t)));
onToggle?.(id, value);
}
function handleRun() {
if (running) return;
setRunning(true);
onRunAgent?.();
setTimeout(() => setRunning(false), 2200);
}
return (
<div className={cn("w-full max-w-xs overflow-hidden rounded-2xl border bg-card", className)}>
<div className="flex items-start justify-between gap-3 px-4 pb-3 pt-4">
<div className="flex min-w-0 items-center gap-2.5">
{agent && <Avatar agent={agent} />}
<div className="min-w-0">
<p className="text-sm font-semibold">{title}</p>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
</div>
<button
type="button"
onClick={handleRun}
disabled={running}
className={cn(
"flex shrink-0 items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors disabled:pointer-events-none",
running ? "border-transparent bg-foreground text-background" : "hover:bg-accent"
)}
>
<Play className={cn("size-3 transition-all", running ? "fill-current" : "fill-none stroke-current")} />
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={running ? "running" : "idle"}
initial={{ opacity: 0, y: 3 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -3 }}
transition={{ duration: 0.1 }}
>
{running ? "Running…" : "Run agent"}
</motion.span>
</AnimatePresence>
</button>
</div>
<ul className="divide-y border-t">
{triggers.map((trigger) => (
<li key={trigger.id} className="flex items-center gap-3 px-4 py-2.5">
<span className="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground">
<trigger.icon className="size-3.5" />
</span>
<p className="min-w-0 flex-1 text-xs leading-snug">
<span className="font-medium">{trigger.event}</span>{" "}
<span className="text-muted-foreground">{trigger.context}</span>
</p>
<div className="flex shrink-0 items-center gap-2">
{trigger.needsSetup && !trigger.enabled && (
<button
type="button"
onClick={() => onFinishSetup?.(trigger.id)}
className="text-[11px] text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground"
>
Finish setup
</button>
)}
<Toggle enabled={trigger.enabled} onChange={(v) => handleToggle(trigger.id, v)} />
</div>
</li>
))}
</ul>
<div className="flex items-center gap-2 border-t px-4 py-2.5">
<button
type="button"
onClick={() => onAddTrigger?.()}
className="flex items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground"
>
<Plus className="size-3.5" />
Add trigger
</button>
<div className="ml-0.5 flex items-center gap-0.5">
{QUICK_ADD_PLATFORMS.map(({ id, label, icon: Icon }) => (
<button
key={id}
type="button"
aria-label={`Add ${label} trigger`}
onClick={() => onAddTrigger?.(id)}
className="flex size-5 items-center justify-center rounded text-muted-foreground/50 transition-colors hover:text-muted-foreground"
>
<Icon className="size-3.5" />
</button>
))}
</div>
</div>
</div>
);
}
A UX spec for this pattern — written for agents implementing or reusing it, not the code.
# Agent Triggers
## Summary
A configuration panel listing the external events that cause an agent to run automatically, each with an on/off toggle and an optional "Finish setup" prompt for incomplete integrations. A "Run agent" button lets the user fire the agent on demand outside of any trigger, and a footer row provides one-click entry points for adding new trigger sources.
## When to use
- In an agent or automation settings panel where users define when a workflow should start.
- When an agent can be connected to multiple external sources (GitHub, Slack, Figma, etc.) and users need to pick which ones activate it.
- Alongside an agent canvas or workflow builder, as the entry-point node configuration.
## When not to use
- For simple scheduled or time-based recurrence — use a cron or schedule picker instead; event-based and time-based triggers have different mental models and should not share this component.
- When there is only ever one possible trigger — a list of one is not a list; use a single connection widget or toggle.
- As a general settings list. This component is about activation conditions, not preferences.
## Anatomy
- Header: a title ("Triggers") and a subtitle ("When should this agent run?") scoped to the agent being configured.
- Run agent button: an outlined play button in the header that fires the agent manually, independent of any trigger. It enters a transient "Running…" state while the agent executes.
- Trigger list: one row per trigger, each containing an icon that identifies the source, an event name in medium weight followed by a muted location context ("in Slack", "in carlosmarch/ai-patterns"), and a toggle on the trailing edge.
- Finish setup label: a small underlined text link that appears before the toggle when a trigger has been added but not yet fully configured. Enables the trigger rather than completing setup inline — that flow belongs in a dedicated integration modal.
- Add trigger footer: a "+ Add trigger" text button plus icon shortcuts for common source platforms (GitHub, Slack, Figma). Both lead to the same source-picker; the icons are just accelerators for recognized integrations.
## Behavior
- Toggling a trigger on/off is immediate and optimistic — the UI updates before the server round-trip. If the save fails, revert and surface an error.
- "Finish setup" shows only when `needsSetup` is true and the trigger is currently off. Once the user enables the trigger (completing setup), the label disappears.
- Run agent is a one-shot control: clicking it disables the button and swaps its label to "Running…" until the run resolves, then restores the idle state. It does not reflect ongoing trigger-based runs.
- The trigger list is non-reorderable — triggers fire in parallel on any matching event, so order has no semantic meaning.
- Adding a new trigger appends it to the list in a needs-setup state with the toggle off; the "Finish setup" label appears immediately.
## Content guidelines
- Event names are short noun phrases or gerunds that describe the inbound event, not the agent's response to it: "Issue labeled pattern-request", "Frame marked ready for dev". Start with the noun (Issue, Frame, Message) so items scan consistently when the list grows.
- The context string names the specific location in sentence-case with no trailing period: "in carlosmarch/ai-patterns", "in any Figma file".
- Platform icons should match the brand icon for the integration, not a generic glyph. When a brand icon isn't available, use a domain-appropriate generic (a hashtag for channels, an @ for mention events).
## Accessibility
- The toggle must be a `<button role="switch" aria-checked={enabled}>` — not a styled `<div>` — so screen readers announce the state change.
- Each toggle must have an accessible label. Since the label is provided by the adjacent row text, use `aria-labelledby` pointing to the event name element, or wrap the row in a `<label>`.
- "Finish setup" must be keyboard reachable and announce where it leads: `aria-label="Finish setup for Frame marked ready for dev"`.
- The quick-add platform icons in the footer are icon-only buttons; each must carry an `aria-label` naming the platform ("Add GitHub trigger").
## Related patterns
- **Flowchart** — Use it as well when the work after the trigger has conditions or branches that need a canvas to read.
- **Tool Approval** — Use it instead when the question is whether the agent may run one specific call, not what events start it.
- **Setup Checklist** — Use it instead when onboarding a whole agent for the first time, and keep Agent Triggers for managing an existing agent's event sources.
Do’s and don’ts
Do use it for
- In an agent or automation settings panel where users define when a workflow should start.
- When an agent can be connected to multiple external sources (GitHub, Slack, Figma, etc.) and users need to pick which ones activate it.
- Alongside an agent canvas or workflow builder, as the entry-point node configuration.
Don’t use it for
- For simple scheduled or time-based recurrence — use a cron or schedule picker instead; event-based and time-based triggers have different mental models and should not share this component.
- When there is only ever one possible trigger — a list of one is not a list; use a single connection widget or toggle.
- As a general settings list. This component is about activation conditions, not preferences.
Related patterns
Flowchart
Use it as well when the work after the trigger has conditions or branches that need a canvas to read.
Tool Approval
Use it instead when the question is whether the agent may run one specific call, not what events start it.
Setup Checklist
Use it instead when onboarding a whole agent for the first time, and keep Agent Triggers for managing an existing agent's event sources.