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.input(z.null()).render(() => <div>Create</div>),
updateStatus: commandBuilder.input(projectBulkSchema).render(() => <div>Status</div>),
archive: commandBuilder.input(projectBulkSchema).render(() => <ArchiveCommand ... />),
restore: commandBuilder.input(projectBulkSchema).render(() => <RestoreCommand ... />),
delete: commandBuilder.input(projectBulkSchema).render(() => <DeleteCommand ... />),
});Select At Usage Site
Use .pick({...}).toArray() directly in menu props.
<InlineCommandMenu data={null} commands={projectCommands.pick({ create: true }).toArray()} />
<ResponsiveCommandMenu
data={project}
commands={projectCommands.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.
const rowSchema = z.looseObject({ id: z.string() });
const bulkSchema = z.union([rowSchema.transform((item) => [item]), rowSchema.array()]);
export const customerCommands = defineCommands({
delete: commandBuilder.input(bulkSchema).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.
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
.input(z.object({ projectId: z.string() }))
.render(({ data, meta }) => (
<CreateTimeEntryCommand
defaultValues={data}
fieldVisibility={{ projectId: meta.fieldVisibility.projectId ?? true }}
/>
)),
});
<InlineCommandMenu
data={{ projectId: "p_1" }}
meta={{ fieldVisibility: { projectId: false } }}
commands={timeEntryCommands.pick({ create: true }).toArray()}
/>Context-Specific Action Sets
Use different subsets for list/detail/child screens.
// list page
commands={taskCommands.pick({ updateStatus: true, updateAssignee: true, delete: true }).toArray()}
// toolbar create action
commands={taskCommands.pick({ create: true }).toArray()}Visibility And Permissions Together
Use permissions for coarse access and visibility for runtime eligibility.
archive: commandBuilder
.input(projectBulkSchema)
.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 validated 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 z from "zod";
import { orpc } from "@/server/router/client";
const createProjectSchema = z.object({ customerId: z.string() });
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>
);
}
export const projectCommands = defineCommands({
create: commandBuilder
.input(createProjectSchema)
.permission({ project: ["create"] })
.render(({ data }) => <CreateProjectCommand customerId={data.customerId} />),
});Render it where the action belongs:
<InlineCommandMenu
data={{ customerId: customer.id }}
commands={projectCommands.pick({ create: true }).toArray()}
/>Which Approach To Choose
- Inline
.pick()at call site: default and preferred .omit(): when a screen uses almost everything except a small subset- Custom local constant in component: acceptable if reused multiple times in the same file only
Avoid global alias exports that only mirror .pick(...) unless there is a strong reuse reason.