{"slug":"agent-triggers","category":"agents","job":"steer","added":"2026-09-15T08:39Z","title":"Agent Triggers","description":"A configuration panel for listing and toggling the external events that activate an agent, with a manual run control.","url":"https://ai-patterns.app/patterns/agents/agent-triggers","markdown":"https://ai-patterns.app/patterns/agents/agent-triggers.md","json":"https://ai-patterns.app/api/patterns/agents/agent-triggers","seenIn":[{"team":"Notion","source":"https://www.notion.com/help/custom-agents"},{"team":"Cursor","source":"https://docs.cursor.com/slack"}],"spec":"# Agent Triggers\n\n## Summary\nA 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.\n\n## When to use\n- In an agent or automation settings panel where users define when a workflow should start.\n- When an agent can be connected to multiple external sources (GitHub, Slack, Figma, etc.) and users need to pick which ones activate it.\n- Alongside an agent canvas or workflow builder, as the entry-point node configuration.\n\n## When not to use\n- 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.\n- When there is only ever one possible trigger — a list of one is not a list; use a single connection widget or toggle.\n- As a general settings list. This component is about activation conditions, not preferences.\n\n## Anatomy\n- Header: a title (\"Triggers\") and a subtitle (\"When should this agent run?\") scoped to the agent being configured.\n- 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.\n- 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.\n- 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.\n- 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.\n\n## Behavior\n- 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.\n- \"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.\n- 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.\n- The trigger list is non-reorderable — triggers fire in parallel on any matching event, so order has no semantic meaning.\n- Adding a new trigger appends it to the list in a needs-setup state with the toggle off; the \"Finish setup\" label appears immediately.\n\n## Content guidelines\n- 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.\n- The context string names the specific location in sentence-case with no trailing period: \"in carlosmarch/ai-patterns\", \"in any Figma file\".\n- 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).\n\n## Accessibility\n- The toggle must be a `<button role=\"switch\" aria-checked={enabled}>` — not a styled `<div>` — so screen readers announce the state change.\n- 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>`.\n- \"Finish setup\" must be keyboard reachable and announce where it leads: `aria-label=\"Finish setup for Frame marked ready for dev\"`.\n- The quick-add platform icons in the footer are icon-only buttons; each must carry an `aria-label` naming the platform (\"Add GitHub trigger\").\n\n## Related patterns\n- **Flowchart** — Use it as well when the work after the trigger has conditions or branches that need a canvas to read.\n- **Tool Approval** — Use it instead when the question is whether the agent may run one specific call, not what events start it.\n- **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.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { AtSign, GitBranch, GitPullRequest, Layers, Play, Plus } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface AgentTrigger {\n  id: string;\n  icon: React.ComponentType<{ className?: string }>;\n  event: string;\n  context: string;\n  enabled: boolean;\n  needsSetup?: boolean;\n}\n\nexport interface AgentAvatar {\n  name: string;\n  src?: string;\n  icon?: React.ComponentType<{ className?: string }>;\n}\n\nexport interface AgentTriggersProps {\n  title?: string;\n  description?: string;\n  agent?: AgentAvatar;\n  triggers?: AgentTrigger[];\n  onRunAgent?: () => void;\n  onToggle?: (id: string, enabled: boolean) => void;\n  onAddTrigger?: (platformId?: string) => void;\n  onFinishSetup?: (id: string) => void;\n  className?: string;\n}\n\nconst QUICK_ADD_PLATFORMS = [\n  { id: \"github\", label: \"GitHub\", icon: GitBranch },\n  { id: \"slack\", label: \"Slack\", icon: AtSign },\n  { id: \"figma\", label: \"Figma\", icon: Layers },\n] as const;\n\nexport const DEFAULT_TRIGGERS: AgentTrigger[] = [\n  {\n    id: \"github-issue\",\n    icon: GitPullRequest,\n    event: \"Issue labeled pattern-request\",\n    context: \"in carlosmarch/ai-patterns\",\n    enabled: true,\n  },\n  {\n    id: \"slack-mention\",\n    icon: AtSign,\n    event: \"When AI-Patterns is mentioned\",\n    context: \"in #design-requests\",\n    enabled: true,\n  },\n  {\n    id: \"figma-ready\",\n    icon: Layers,\n    event: \"Frame marked ready for dev\",\n    context: \"in any Figma file\",\n    enabled: false,\n    needsSetup: true,\n  },\n];\n\nfunction Avatar({ agent }: { agent: AgentAvatar }) {\n  return (\n    <span className=\"flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-muted-foreground\">\n      {agent.src ? (\n        // eslint-disable-next-line @next/next/no-img-element\n        <img src={agent.src} alt={agent.name} className=\"size-full object-cover\" />\n      ) : agent.icon ? (\n        <agent.icon className=\"size-4\" />\n      ) : (\n        <span className=\"text-xs font-semibold\">\n          {agent.name.split(\" \").map((w) => w[0]).slice(0, 2).join(\"\").toUpperCase()}\n        </span>\n      )}\n    </span>\n  );\n}\n\nfunction Toggle({ enabled, onChange }: { enabled: boolean; onChange: (v: boolean) => void }) {\n  return (\n    <button\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={enabled}\n      onClick={() => onChange(!enabled)}\n      className={cn(\n        \"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\",\n        enabled ? \"bg-foreground\" : \"bg-muted\"\n      )}\n    >\n      <motion.span\n        aria-hidden\n        className=\"pointer-events-none inline-block h-4 w-4 rounded-full bg-background shadow-sm\"\n        animate={{ x: enabled ? 16 : 0 }}\n        transition={{ type: \"spring\", stiffness: 500, damping: 30 }}\n      />\n    </button>\n  );\n}\n\nexport function AgentTriggers({\n  title = \"Triggers\",\n  description = \"When should this agent run?\",\n  agent,\n  triggers: initialTriggers = DEFAULT_TRIGGERS,\n  onRunAgent,\n  onToggle,\n  onAddTrigger,\n  onFinishSetup,\n  className,\n}: AgentTriggersProps) {\n  const [triggers, setTriggers] = React.useState(initialTriggers);\n  const [running, setRunning] = React.useState(false);\n\n  function handleToggle(id: string, value: boolean) {\n    setTriggers((prev) => prev.map((t) => (t.id === id ? { ...t, enabled: value } : t)));\n    onToggle?.(id, value);\n  }\n\n  function handleRun() {\n    if (running) return;\n    setRunning(true);\n    onRunAgent?.();\n    setTimeout(() => setRunning(false), 2200);\n  }\n\n  return (\n    <div className={cn(\"w-full max-w-xs overflow-hidden rounded-2xl border bg-card\", className)}>\n      <div className=\"flex items-start justify-between gap-3 px-4 pb-3 pt-4\">\n        <div className=\"flex min-w-0 items-center gap-2.5\">\n          {agent && <Avatar agent={agent} />}\n          <div className=\"min-w-0\">\n            <p className=\"text-sm font-semibold\">{title}</p>\n            <p className=\"text-xs text-muted-foreground\">{description}</p>\n          </div>\n        </div>\n        <button\n          type=\"button\"\n          onClick={handleRun}\n          disabled={running}\n          className={cn(\n            \"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\",\n            running ? \"border-transparent bg-foreground text-background\" : \"hover:bg-accent\"\n          )}\n        >\n          <Play className={cn(\"size-3 transition-all\", running ? \"fill-current\" : \"fill-none stroke-current\")} />\n          <AnimatePresence mode=\"wait\" initial={false}>\n            <motion.span\n              key={running ? \"running\" : \"idle\"}\n              initial={{ opacity: 0, y: 3 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: -3 }}\n              transition={{ duration: 0.1 }}\n            >\n              {running ? \"Running…\" : \"Run agent\"}\n            </motion.span>\n          </AnimatePresence>\n        </button>\n      </div>\n\n      <ul className=\"divide-y border-t\">\n        {triggers.map((trigger) => (\n          <li key={trigger.id} className=\"flex items-center gap-3 px-4 py-2.5\">\n            <span className=\"flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground\">\n              <trigger.icon className=\"size-3.5\" />\n            </span>\n            <p className=\"min-w-0 flex-1 text-xs leading-snug\">\n              <span className=\"font-medium\">{trigger.event}</span>{\" \"}\n              <span className=\"text-muted-foreground\">{trigger.context}</span>\n            </p>\n            <div className=\"flex shrink-0 items-center gap-2\">\n              {trigger.needsSetup && !trigger.enabled && (\n                <button\n                  type=\"button\"\n                  onClick={() => onFinishSetup?.(trigger.id)}\n                  className=\"text-[11px] text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground\"\n                >\n                  Finish setup\n                </button>\n              )}\n              <Toggle enabled={trigger.enabled} onChange={(v) => handleToggle(trigger.id, v)} />\n            </div>\n          </li>\n        ))}\n      </ul>\n\n      <div className=\"flex items-center gap-2 border-t px-4 py-2.5\">\n        <button\n          type=\"button\"\n          onClick={() => onAddTrigger?.()}\n          className=\"flex items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground\"\n        >\n          <Plus className=\"size-3.5\" />\n          Add trigger\n        </button>\n        <div className=\"ml-0.5 flex items-center gap-0.5\">\n          {QUICK_ADD_PLATFORMS.map(({ id, label, icon: Icon }) => (\n            <button\n              key={id}\n              type=\"button\"\n              aria-label={`Add ${label} trigger`}\n              onClick={() => onAddTrigger?.(id)}\n              className=\"flex size-5 items-center justify-center rounded text-muted-foreground/50 transition-colors hover:text-muted-foreground\"\n            >\n              <Icon className=\"size-3.5\" />\n            </button>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n","dependencies":["lucide-react","motion"],"localImports":["@/lib/utils"]}