Tulip Logo IconTulip
Commands

Intro

What Commands are, how to define them, and how to compose them locally.

Tulip Commands are a typed action system for business UIs. You define each command once as a reusable value, then compose the commands you need where they are rendered.

Entry points

import { commandBuilder, createCommandBuilder, ensureArray } from "@tulip-systems/commands";
import { InlineCommandMenu, DeleteCommand } from "@tulip-systems/commands/client";
  • @tulip-systems/commands: command definitions and helpers
  • @tulip-systems/commands/client: menu components, command primitives, and hooks
  • Atomic command: one exported command value such as projectArchiveCommand
  • Local composition: pass an inline array of command values to the menu where they are used
  • Shared normalization: use .transform(ensureArray) when one command must support single-item and multi-item inputs

Why this pattern

This keeps the system explicit and boring:

  • action behavior stays centralized in the command definition
  • each screen still clearly shows which commands it renders
  • no tag systems, registries, or query-style command selection APIs are needed
  • permissions, visibility, disabled state, and mutation side-effects stay with the command itself

First command

import { commandBuilder } from "@tulip-systems/commands";
import { CommandClick, CommandLabel, InlineCommandMenu } from "@tulip-systems/commands/client";
import { ArchiveIcon } from "lucide-react";
import { orpc } from "@/server/router/client";

type ProjectData = { id: string };

export const projectArchiveCommand = commandBuilder
  .$type<ProjectData>()
  .permission({ project: ["archive"] })
  .render(({ data }) => (
    <CommandClick
      label="Archive"
      variables={{ ids: [data.id] }}
      mutation={orpc.projects.archive.mutationOptions()}
    >
      <ArchiveIcon className="w-4" />
      <CommandLabel />
    </CommandClick>
  ));

export function ProjectActions({ project }: { project: ProjectData }) {
  return <InlineCommandMenu data={project} commands={[projectArchiveCommand]} />;
}

Local composition

Compose command arrays where the UI renders them:

<ResponsiveCommandMenu
  data={project}
  commands={[
    projectUpdateStatusCommand,
    projectArchiveCommand,
    projectRestoreCommand,
    projectDeleteCommand,
  ]}
/>

For a create action in a toolbar:

<InlineCommandMenu data={null} commands={[projectCreateCommand]} />

If the same array is reused often, you can extract a nearby local constant, but avoid vague exported groups like singleCommands, bulkCommands, or globalCommands.

Common helpers

  • commandBuilder: define a command with typed input data
  • createCommandBuilder<TMeta>(): define commands that also need shared meta
  • ensureArray: normalize T | T[] into T[]
  • menu components: InlineCommandMenu, DropdownCommandMenu, ContextCommandMenuContent, FloatingCommandMenu, ResponsiveCommandMenu
  • utility primitives: DeleteCommand, ArchiveCommand, RestoreCommand, CommandClick, CommandDialog, CommandFormDialog

When a command does not render

Check these in order:

  • the menu received an empty commands array
  • .visibleWhen(...) returned false or an array containing false
  • the current user does not satisfy .permission(...)
  • the command rendered but is disabled by .disabledWhen(...)

On this page