AI Patterns

agents

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 ? (
        <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: the canvas-level counterpart — Agent Triggers configures the entry-point event, while the Flowchart node describes what conditions gate the work that follows.
- Tool Approval: also governs what an agent is allowed to do, but at the per-call level rather than the activation level.
- Setup Checklist: shares the "incomplete step" affordance (the Finish setup label) — use Setup Checklist when onboarding a whole agent for the first time; use Agent Triggers when managing an already-running agent's event sources.