Skip to main content

Action design rules

Use these rules to keep behavior predictable across better-cmdk and modifywithai.
  1. Define one canonical actions: CommandAction[] array.
  2. Use one action per underlying operation (no duplicates).
  3. Write specific description text so AI routing is reliable.
  4. Add inputSchema only when the action needs arguments.
  5. Mark sensitive operations with approvalRequired: true.
If a field is irrelevant to one library, that library ignores it. By default, CommandMenu uses https://better-cmdk.com/api/chat as a free developer trial endpoint (no signup, 10 requests per 10 minutes). For production, set your own chatEndpoint or use modifywithai for agentic capabilities. You can disable chat with chatEndpoint={null}.

CommandAction reference

FieldTypeRequiredUsed by
namestringYesboth
descriptionstringYesboth
execute(options: Record<string, unknown>) => voidYesboth
labelstringNobetter-cmdk
groupstringNobetter-cmdk
iconReactNodeNobetter-cmdk
shortcutstringNobetter-cmdk
keywordsstring[]Nobetter-cmdk
semanticKeystringNoboth (overlap identity)
disabledbooleanNobetter-cmdk
onSelect() => voidNobetter-cmdk
inputSchemaRecord<string, { type; description?; required? }>Noboth
approvalRequiredbooleanNoagent providers

Quality examples

{
  name: "archive-project",
  label: "Archive project",
  description:
    "Move a project to the archive list. The project can be restored later.",
  group: "Projects",
  approvalRequired: true,
  execute: ({ projectId }) => archiveProject(String(projectId)),
  inputSchema: {
    projectId: {
      type: "string",
      description: "ID of the project to archive",
      required: true,
    },
  },
}

Command-like vs argument-requiring

Action kindDefinitionDirect select behavior
Command-likeNo inputSchemaCalls onSelect or execute({})
Argument-requiringHas inputSchemaRouted into chat so arguments can be gathered
Do not create both forms for the same operation.

Shared-array integration with modifywithai

Reuse the same actions list in both libraries:
import { CommandMenu, type CommandAction } from "better-cmdk";
import { useAssistant } from "modifywithai";

const actions: CommandAction[] = [
  {
    name: "toggle-dark-mode",
    label: "Toggle dark mode",
    description: "Toggle between light and dark theme",
    group: "Appearance",
    execute: () => document.documentElement.classList.toggle("dark"),
  },
  {
    name: "navigate-to",
    label: "Navigate to page",
    description: "Navigate to a specific path in the app",
    inputSchema: {
      path: { type: "string", description: "Destination path", required: true },
    },
    execute: ({ path }) => router.push(String(path)),
  },
];

const assistant = useAssistant({
  tokenEndpoint: "/api/mwai/token",
  actions,
});

<CommandMenu open={open} onOpenChange={setOpen} actions={actions} chat={assistant} />;

Custom rendering (composition API)

Use children for full UI control:
import {
  CommandMenu,
  CommandInput,
  CommandList,
  CommandGroup,
  CommandItem,
  CommandEmpty,
  CommandShortcut,
} from "better-cmdk";

<CommandMenu open={open} onOpenChange={setOpen}>
  {({ mode }) => (
    <>
      <CommandInput
        placeholder={mode === "chat" ? "Ask AI..." : "Search actions..."}
        showSendButton
      />
      <CommandList>
        <CommandGroup heading="Navigation">
          <CommandItem onSelect={() => router.push("/dashboard")}>
            Go to Dashboard
            <CommandShortcut>⌘D</CommandShortcut>
          </CommandItem>
        </CommandGroup>
        <CommandEmpty />
      </CommandList>
    </>
  )}
</CommandMenu>
When actions and children are both provided, actions takes precedence.

Approval workflow

For external agents, approvalRequired actions can be gated by user confirmation. Flow:
  1. Agent requests execution of one or more actions.
  2. UI asks the user to approve or deny.
  3. Your app sends the decision with addToolApprovalResponse.
const { addToolApprovalResponse } = useCommandMenuContext();

function approve(id: string) {
  addToolApprovalResponse?.({ id, approved: true });
}

function deny(id: string) {
  addToolApprovalResponse?.({ id, approved: false });
}
AssistantMessages renders built-in confirmation UI for standard chat mode. Use custom confirmation components only when you need custom presentation.

Context and history hooks

useCommandMenuContext() exposes runtime state (mode, messages, status, sendMessage, switchToChat, switchToCommand). Use historyStorageKey and maxConversations on CommandMenu to control persisted chat history:
<CommandMenu
  open={open}
  onOpenChange={setOpen}
  actions={actions}
  historyStorageKey="my-app-command-history"
  maxConversations={20}
/>

Theming and styling

Customize tokens on .bcmdk-root:
.bcmdk-root {
  --bcmdk-primary: 0.205 0 0;
  --bcmdk-primary-foreground: 0.985 0 0;
  --bcmdk-muted: 0.97 0 0;
  --bcmdk-border: 0.922 0 0;
  --bcmdk-radius: 0.625rem;
}
Then use className props for local overrides, and cn() for conditional classes.

Next steps

ModifyWithAI concepts

Deep dive into action routing, context, and approvals.

GitHub

Browse source, open issues, or contribute improvements.