Skip to main content

Action Configuration Reference

Actions are the buttons and menu items that do something to a record: delete a pipeline, retry a run, open an "Update" form. You configure them as ActionConfigSchema objects on an entity (PhaseEntityConfig.actions) or on a table (TableConfig.actions). A separate CreateActionConfig (PhaseEntityConfig.createAction) adds a page-level "Create …" button.

This page is for contributors adding or changing record actions in Michelangelo AI Studio. It assumes you've read the Entity Configuration Reference.

The types on this page are exported from @michelangelo-ai/core. The examples use the #core/... aliases that code inside packages/core uses; from an app, import from @michelangelo-ai/core.

Where Actions Render​

ConfigWhere it rendersInterpolation context
PhaseEntityConfig.actionsEach row of the entity's list view table, and the detail view headerrow in the list, page in the header
TableConfig.actionsEach row of a detail page table (type: 'table' pages). Ignored on the list view, see belowrow
PhaseEntityConfig.createActionA button in the list table's action bar (trailing slot), only while that entity's tab is activenone (no record)
warning
List view: entity actions replace tableConfig.actions

PhaseEntityView builds the list table from views[0].tableConfig and then overwrites actions with entity.actions:

tableConfig={{ ...currentEntityConfig.views[0].tableConfig, actions: entity.actions }}

So on the list view, any actions you put in the list view's tableConfig are always discarded, even when the entity has no actions (the override sets undefined). Put list-row actions on the entity. Two more consequences:

  • The list view must be views[0].
  • TableConfig.actions still works for tables inside detail pages.

Hierarchy only affects the detail header. In table rows (list view and detail-page tables), every action goes into the row's overflow menu, whatever its hierarchy. In the detail header:

hierarchyDetail header rendering
ActionHierarchy.PRIMARYA filled primary button. Only the first PRIMARY action is rendered; any others are dropped
ActionHierarchy.SECONDARYA pill-shaped secondary button, one per action
ActionHierarchy.TERTIARYAn item in the overflow (…) menu
(unset)Same as TERTIARY: actions without a hierarchy go to the overflow menu

Write interpolations against data, not row or page. Entity actions are resolved with { row: record } in list rows and { page: record } in the detail header. The interpolation context's data is row ?? page, so ({ data }) => … or ${data.metadata.name} works in both places. Every built-in entity action uses data.

ActionConfig is ActionConfigBase plus one of three shapes. TypeScript rejects any other combination (modal?: never / operation?: never).

#operationmodalWhat happens on click
1MutationActionConfig or RouteActionConfigConfirmModalConfigA confirm dialog opens. Confirming runs the mutation, or navigates to the route
2MutationActionConfig or RouteActionConfig(none)A route navigates immediately. A mutation does nothing; see the warning below
3(none)CustomModalConfigYour component opens with { record, onClose } and owns the whole submit flow

You can't pair a custom modal with an operation. If a custom form needs to run a mutation, call useStudioMutation inside the component, as CreatePipelineRunForm and DeploymentForm do.

A mutation without a modal doesn't run

Shape 2 type-checks with a mutation, but neither the row menu nor the header buttons dispatch it. Their click handlers only open a modal or follow a route. Always pair a mutation with a ConfirmModalConfig (shape 1). Every built-in mutation action does.

ActionConfigBase​

PropertyTypeDescriptionDefault/Required
displayActionTriggerDisplay{ label: string; icon?: string }. icon is the name of an icon registered in the IconProvider✅ Required
hierarchyActionHierarchyPRIMARY, SECONDARY or TERTIARY. See Where Actions RenderTertiary (overflow menu)
disabled{ condition: boolean; message?: string }[]Rules that disable the action for a recordundefined (enabled)

disabled​

Rules are evaluated in order. The first rule whose condition is true disables the action, and its message is shown as a tooltip on hover. The condition is usually a function interpolation over the record:

// isRetryable is a helper you write for your own record type.
disabled: [
{
condition: interpolate(({ data }) => !isRetryable(data)),
message: 'Only failed or killed runs can be retried',
},
],

Operations​

MutationActionConfig​

{ type: 'mutation'; mutation: MutationConfig }. The record is sent as the request payload, after middleware.

MutationConfig propertyTypeDescriptionRequired
mutationNamestringRPC handler name, e.g. 'DeletePipeline', 'UpdateDeployment'✅ Required
clientOptionsMutationOptionsonSuccess(data) / onError(error) callbacksOptional
successOperationsSuccessOperation[]Side effects to run, in order, after the mutation succeedsOptional
middlewareMiddlewareSchemaReshapes the record before the request is sent (operations with source, destination, default, transformation; plus scaffold). See MiddlewareSchema in packages/core/hooks/use-schema-middleware/types.ts for the full surfaceOptional

RouteActionConfig​

{ type: 'route'; route: string }. Navigates to route, which can be interpolated, e.g. '/${studio.projectId}/${studio.phase}/runs/${data.metadata.name}'.

Success Operations​

Nothing is invalidated automatically after a mutation. If a list or detail page should refetch, add an invalidate operation for its query.

typePropertiesBehavior
'invalidate'targets: (string | { name: string; serviceOptions: Record<string, unknown> })[], delayMs?: numberInvalidates queries by name (e.g. 'ListPipeline'), or by name plus service options. delayMs waits before invalidating; use it when a controller reconciles the change asynchronously after the API responds
'toast'message: string, icon?: string, action?: { label: string; route?: string }Shows a snackbar. icon defaults to 'checkCircle'. An action with a route navigates when clicked; without a route it dismisses the toast
'route'route: stringNavigates without showing a toast

Success operations are resolved against the mutation result, so they can read ${response.…}:

{
type: 'toast',
message: 'A new pipeline run has been created from the failed step of this run.',
action: {
label: 'See new run',
route: interpolate(
'/${studio.projectId}/${studio.phase}/runs/${response.pipelineRun.metadata.name}'
),
},
},

Modals​

ConfirmModalConfig (type: 'confirm')​

PropertyTypeDescriptionRequired
type'confirm'Discriminator✅ Required
header.titlestringDialog heading✅ Required
bodyReactNodeDialog body, rendered through Markdown (so **bold** in a string works)Optional
bannerBannerConfig{ content: ReactNode; kind?: BannerProps['kind']; icon?: string }, shown above the bodyOptional
button.labelstringConfirm button label✅ Required
button.iconstringAccepted by the type, but ConfirmDispatcher doesn't pass it to the dialog, so it isn't renderedOptional
destructivebooleanRenders the confirm button in red. Use for irreversible actions such as deletefalse
sizeSize from baseui/dialogDialog sizeOptional

CustomModalConfig (type: 'custom')​

PropertyTypeDescriptionRequired
type'custom'Discriminator✅ Required
componentComponentType<ActionComponentProps<T>>Rendered when the action is clicked. Props: record: T, onClose: () => void✅ Required

The component renders its own dialog, usually a FormDialog, and calls onClose when done.

CreateActionConfig​

A page-level "Create …" button. It has no record, so it isn't an ActionConfig: there's no operation, hierarchy or disabled.

PropertyTypeDescriptionRequired
displayActionTriggerDisplayButton label and optional icon✅ Required
componentComponentType<CreateActionComponentProps>Rendered when the button is clicked. Props: onClose: () => void✅ Required
// From: config/entities/targets/target.ts
createAction: {
display: { label: 'Create target', icon: 'plus' },
component: CreateInferenceServerForm,
},

Interpolation​

Config authors write ActionConfigSchema<T>, which is DeepInterpolatable<ActionConfig<T>>: every leaf except functions and React components can be a plain value, a ${…} string or an interpolate(...) call. The schema is resolved per record before any rendering code sees it. Components receive the resolved ActionConfig. See Interpolation System for the context fields (data, row, page, studio, response).

Example​

Trimmed from config/entities/pipeline/shared.ts. Every built-in pattern appears here:

  • PIPELINE_DELETE_ACTION: a mutation, middleware, three ordered success operations (one delayed), and a destructive confirm with an interpolated body.
  • PIPELINE_ACTIONS: custom modals and a disabled rule.
import { ActionHierarchy } from '#core/components/actions/types';
import { interpolate } from '#core/interpolation/interpolate';

import type { ActionConfigSchema } from '#core/components/actions/types';

// pipelineName, hasNoTriggers, CreatePipelineRunForm and RunTriggerForm are
// defined elsewhere in the source file.
export const PIPELINE_DELETE_ACTION: ActionConfigSchema<object> = {
display: { label: 'Delete', icon: 'trashCan' },
hierarchy: ActionHierarchy.TERTIARY,
operation: {
type: 'mutation',
mutation: {
mutationName: 'DeletePipeline',
middleware: {
operations: [
{ source: 'spec.baseResource', destination: 'metadata', transformation: (v) => v },
],
},
successOperations: [
{ type: 'invalidate', targets: ['ListPipeline'] },
// Revisions are cascade-deleted asynchronously, so wait before refetching them.
{ type: 'invalidate', targets: ['ListRevision'], delayMs: 2000 },
{ type: 'route', route: '/${studio.projectId}/${studio.phase}/pipelines' },
],
},
},
modal: {
type: 'confirm',
header: { title: 'Delete Pipeline' },
body: interpolate(({ data }) => `Delete pipeline **${pipelineName(data)}**? This action cannot be undone.`),
button: { label: 'Delete' },
destructive: true,
},
};

export const PIPELINE_ACTIONS: ActionConfigSchema<object>[] = [
{
display: { label: 'Run', icon: 'playerPlay' },
hierarchy: ActionHierarchy.PRIMARY,
modal: { type: 'custom', component: CreatePipelineRunForm },
},
{
display: { label: 'Run trigger', icon: 'calendarRepeat' },
hierarchy: ActionHierarchy.SECONDARY,
disabled: [
{
condition: interpolate(({ data }) => hasNoTriggers(data)),
message: 'No triggers defined for this pipeline',
},
],
modal: { type: 'custom', component: RunTriggerForm },
},
PIPELINE_DELETE_ACTION,
];

pipelineName, hasNoTriggers, CreatePipelineRunForm and RunTriggerForm are pipeline-specific helpers and forms defined next to this config. (pipelineName stands in for the inline expression in the real file.)

Source Files​

Type definitions:

Behavior:

Real examples: