Examples
Real implementation patterns and different approaches across the app.
This page shows practical command architecture patterns used across Tulip apps.
Single Registry Per Domain Module
Define one command registry per feature and reuse everywhere.
// app/admin/projects/_config/commands.tsx
export const projectCommands = defineCommands({
create: commandBuilder.$type<null>().render(() => <div>Create</div>),
updateStatus: commandBuilder.$type<{ id: string }[]>().render(() => <div>Status</div>),
archive: commandBuilder.$type<{ id: string }[]>().render(() => <ArchiveCommand ... />),
restore: commandBuilder.$type<{ id: string }[]>().render(() => <RestoreCommand ... />),
delete: commandBuilder.$type<{ id: string }[]>().render(() => <DeleteCommand ... />),
});Select At Usage Site
Use .keys.pick({...}).toArray() directly in menu props.
<InlineCommandMenu data={null} commands={projectCommands.keys.pick({ create: true }).toArray()} />
<ResponsiveCommandMenu
data={project}
commands={projectCommands.keys.pick({ updateStatus: true, archive: true, restore: true, delete: true }).toArray()}
/>Why this approach:
- selection intent is visible where actions are rendered
- no extra alias constants to maintain
- easier to compare across pages
Bulk And Single Normalization
Accept both row actions and bulk selections in one command with .transform().
import { commandBuilder, defineCommands, ensureArray } from "@tulip-systems/core/commands";
type RowData = { id: string };
type ProjectInput = RowData | RowData[];
export const customerCommands = defineCommands({
delete: commandBuilder
.$type<ProjectInput>()
.transform(ensureArray)
.render(({ data }) => (
<DeleteCommand variables={{ ids: data.map((item) => item.id) }} mutation={...} />
)),
});This keeps table row actions and bulk action bars on the same command implementation without repeating the same array normalization in every command.
Meta-Driven Command Behavior
Use createCommandBuilder<TMeta>() when a command needs view context.
const timeBuilder = createCommandBuilder<{ fieldVisibility: { projectId?: boolean } }>();
export const timeEntryCommands = defineCommands({
create: timeBuilder
.$type<{ projectId: string }>()
.render(({ data, meta }) => (
<CreateTimeEntryCommand
defaultValues={data}
fieldVisibility={{ projectId: meta.fieldVisibility.projectId ?? true }}
/>
)),
});
<InlineCommandMenu
data={{ projectId: "p_1" }}
meta={{ fieldVisibility: { projectId: false } }}
commands={timeEntryCommands.keys.pick({ create: true }).toArray()}
/>Context-Specific Action Sets
Use different subsets for list/detail/child screens.
// list page
commands={taskCommands.keys.pick({ updateStatus: true, updateAssignee: true, delete: true }).toArray()}
// toolbar create action
commands={taskCommands.keys.pick({ create: true }).toArray()}Visibility And Permissions Together
Use permissions for coarse access and visibility for runtime eligibility.
archive: commandBuilder
.$type<{ id: string; deletedAt: string | null }[]>()
.permission({ project: ["archive"] })
.visibleWhen(({ data }) => data.every((item) => item.deletedAt === null))
.render(({ data }) => <ArchiveCommand variables={{ ids: data.map((i) => i.id) }} mutation={...} />)Full Create Command Example
This pattern keeps form state inside the command component and lets the registry pass only typed command data.
import { commandBuilder, defineCommands } from "@tulip-systems/core/commands";
import {
CommandFormDialog,
CommandFormDialogCancel,
CommandFormDialogContent,
CommandFormDialogFields,
CommandFormDialogFooter,
CommandFormDialogHeader,
CommandFormDialogSubmit,
CommandFormDialogTitle,
CommandFormDialogTrigger,
CommandLabel,
} from "@tulip-systems/core/commands/client";
import { Input } from "@tulip-systems/core/components";
import { Form, FormControl, FormField, FormItem, FormLabel } from "@tulip-systems/core/components/client";
import { PlusIcon } from "lucide-react";
import { useForm } from "react-hook-form";
import { orpc } from "@/server/router/client";
type CreateProjectInput = {
name: string;
customerId: string;
};
function CreateProjectCommand({ customerId }: { customerId: string }) {
const form = useForm<CreateProjectInput>({ defaultValues: { name: "", customerId } });
return (
<CommandFormDialog>
<CommandFormDialogTrigger label="Create project">
<PlusIcon className="w-4" />
<CommandLabel />
</CommandFormDialogTrigger>
<Form {...form}>
<CommandFormDialogContent
variables={(values) => values}
mutation={orpc.projects.create.mutationOptions()}
>
<CommandFormDialogHeader>
<CommandFormDialogTitle>Create project</CommandFormDialogTitle>
</CommandFormDialogHeader>
<CommandFormDialogFields>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
</CommandFormDialogFields>
<CommandFormDialogFooter>
<CommandFormDialogCancel>Cancel</CommandFormDialogCancel>
<CommandFormDialogSubmit>Create</CommandFormDialogSubmit>
</CommandFormDialogFooter>
</CommandFormDialogContent>
</Form>
</CommandFormDialog>
);
}
type CreateProjectData = { customerId: string };
export const projectCommands = defineCommands({
create: commandBuilder
.$type<CreateProjectData>()
.permission({ project: ["create"] })
.render(({ data }) => <CreateProjectCommand customerId={data.customerId} />),
});Render it where the action belongs:
<InlineCommandMenu
data={{ customerId: customer.id }}
commands={projectCommands.keys.pick({ create: true }).toArray()}
/>Which Approach To Choose
commands.keys.pick()at call site: default and preferredcommands.keys.omit(): when a screen uses almost everything except a small subsetcommands.tags.every/some/without(): when commands need context-based selection- Custom local constant in component: acceptable if reused multiple times in the same file only
Avoid global alias exports that only mirror commands.keys.pick(...) unless there is a strong reuse reason.