Command Builder
Create command definitions with schema, permission, visibility, disabled state, and render.
Use commandBuilder (or createCommandBuilder<TMeta>()) to define commands in a strict chain.
import { commandBuilder, createCommandBuilder, defineCommands } from "@tulip-systems/core/commands";Builder flow
Every command starts with .input(schema) and ends with .render(fn).
Supported chains:
.input(schema).input(schema).permission(permission).input(schema).visibleWhen(fn).input(schema).disabledWhen(fn).input(schema).permission(permission).visibleWhen(fn).input(schema).permission(permission).disabledWhen(fn)
render is always the final step. You can call permission, visibleWhen, and disabledWhen in any order before render.
.input(schema)
Defines the shape of data the command receives.
It is also a runtime gate: if incoming data does not validate, the command will not render.
import z from "zod";
const singleSchema = z.looseObject({ id: z.string() });
const bulkSchema = z.union([singleSchema.transform((item) => [item]), singleSchema.array()]);
const commands = defineCommands({
delete: commandBuilder.input(bulkSchema).render(({ data }) => <div>{data.length}</div>),
});Use this to normalize single + bulk actions into one array shape.
Because of this validation step, .input(...) protects command UIs from rendering with invalid payloads.
The parsed output of the schema is passed to render, visibleWhen, and disabledWhen, so transforms are safe to use for normalization.
.permission(permission)
Attaches authorization requirements to the command.
Permission is also a render gate: when the current user does not satisfy the permission, the command does not render.
const commands = defineCommands({
create: commandBuilder
.input(z.null())
.permission({ customer: ["create"] })
.render(() => <div>Create</div>),
});Keep permission logic in the command definition so every surface behaves consistently.
Permission checks are applied by the command renderer around the rendered command content. Schema validation and visibility/disabled rules are evaluated before this permission wrapper.
.visibleWhen(({ data, meta }) => ...)
Controls whether the command is rendered for the current context.
const commands = defineCommands({
archive: commandBuilder
.input(bulkSchema)
.visibleWhen(({ data }) => data.every((item) => item.isDeleted === false))
.render(() => <div>Archive</div>),
});Visibility conditions can return a boolean or boolean array. Arrays must pass every(Boolean).
Use visibility for runtime eligibility, such as archived state, readonly rows, or whether all selected records support the action.
conditions(...) remains available as a deprecated alias of visibleWhen(...) for backward compatibility.
.disabledWhen(({ data, meta }) => ...)
Controls whether a command is rendered but disabled.
const commands = defineCommands({
archive: commandBuilder
.input(bulkSchema)
.visibleWhen(({ data }) => data.length > 0)
.disabledWhen(({ data }) => data.some((item) => item.locked))
.render(() => <div>Archive</div>),
});Disabled conditions can return a boolean or boolean array. Arrays must pass every(Boolean).
When disabled, command triggers stay visible but are not interactive.
.render(({ data, meta, ui }) => ...)
Returns the UI for the command.
data: parsed output of.input(...)meta: optional contextual meta passed by the menuui: the current surface (inline,dropdown,context,table,custom)
const commands = defineCommands({
create: commandBuilder.input(z.null()).render(({ ui }) => <div>Rendered in: {ui}</div>),
});createCommandBuilder<TMeta>()
Use this when commands need extra context that is not in data.
import type { VisibilityState } from "@tanstack/react-table";
const builder = createCommandBuilder<{ fieldVisibility: VisibilityState }>();
const timeEntryCommands = defineCommands({
create: builder
.input(z.object({ taskId: z.string() }))
.render(({ data, meta }) => <div>{meta.fieldVisibility.taskId ? data.taskId : "hidden"}</div>),
});Pass meta at render site:
<InlineCommandMenu
data={{ taskId: "task_1" }}
meta={{ fieldVisibility: { taskId: false } }}
commands={timeEntryCommands.pick({ create: true }).toArray()}
/>defineCommands({...})
Wraps command definitions in a registry with helper methods:
.pick({...}).omit({...}).extend({...}).toArray()
export const taskCommands = defineCommands({
create: commandBuilder.input(z.null()).render(() => <div>Create</div>),
delete: commandBuilder.input(bulkSchema).render(() => <div>Delete</div>),
});Use selections where commands are rendered:
commands={taskCommands.pick({ create: true }).toArray()}Selections can use object flags or arrays:
taskCommands.pick({ create: true, delete: true }).toArray();
taskCommands.pick(["create", "delete"]).toArray();
taskCommands.omit({ delete: true }).toArray();
taskCommands.omit(["delete"]).toArray();Use .extend({...}) when a feature needs a local override or extra command while keeping the base registry available.