Skip to main content
configurator.tsx renders the configuration UI inside the workflow editor when a workspace member adds or edits a block. It runs client-side only, with no server access or side-effects. Every block needs a configurator.tsx. It has two responsibilities:
  1. Render inputs for each field in the block’s config schema so workspace members can fill them in.
  2. Declare outcomes via the Outcome component so the editor knows what data each outcome branch exposes to downstream steps.

defineConfigurator

Call defineConfigurator in configurator.tsx, passing the block and a render function. The render function receives the block definition and returns the JSX form.
configurator.tsx
import {Workflows} from "attio/client"
import block from "./block"

export default Workflows.defineConfigurator(block, (workflowBlock) => {
  const {TextInput, Outcome} = Workflows.useConfigurator(workflowBlock)
  return (
    <>
      <TextInput name="project_id" label="Project ID" />
      <Outcome
        id="created"
        label="Task created"
        schema={{task_id: Workflows.OutcomeSchema.string()}}
      />
      <Outcome id="not-found" label="Project not found" schema={null} />
    </>
  )
})

useConfigurator

Call Workflows.useConfigurator(workflowBlock) inside the render function to get typed input components and the Outcome component bound to the block’s config schema. Pass the workflowBlock from the defineConfigurator render callback argument.

Input components

Each input component corresponds to a schema field type. The name prop is type-checked against the schema. TypeScript will error if name doesn’t match a field of the correct type.
ComponentSchema typeDescription
TextInputConfigSchema.string()Single-line text
RichTextInputConfigSchema.richText()Formatted text
NumberInputConfigSchema.number()Numeric value
CheckboxInputConfigSchema.boolean()True/false toggle
ComboboxInputConfigSchema.stringEnum(values)Dropdown from a fixed list
DateInputConfigSchema.date()Calendar date
TimestampInputConfigSchema.timestamp()Date and time
DurationInputConfigSchema.duration()Length of time
EmailAddressInputConfigSchema.emailAddress()Email address
PhoneNumberInputConfigSchema.phoneNumber()Phone number
PersonalNameInputConfigSchema.personalName()First and last name
DomainInputConfigSchema.domain()Web domain
LocationInputConfigSchema.location()Geographic location
CurrencyInputConfigSchema.currency()Monetary value
AttioRecordInputConfigSchema.attioRecord()Attio record
AttioObjectInputConfigSchema.attioObject()Attio object type
AttioListInputConfigSchema.attioList()Attio list
AttioActorInputConfigSchema.attioActor()Attio actor (user or workspace member)
AttioAttributeInputConfigSchema.attioAttribute()Attio attribute
AttioSelectInputConfigSchema.attioSelect()Select value from an Attio attribute
AttioSequenceInputConfigSchema.attioSequence()Attio sequence
CollectionInputConfigSchema.array(element)Repeating list of values
All input components accept at minimum:
PropTypeRequiredDescription
namestringYesPath to the schema field, type-checked against the schema
labelstringYesHuman-readable label shown above the input
helpstringNoHelp text shown below the input
tooltipstringNoTooltip shown on hover
placeholderstringNoPlaceholder text

Outcome component

Every {type: "outcome"} return value carries an id, a short identifier you choose (e.g. "created", "not-found"). Attio exposes each unique id as a named branch in the workflow editor. Workspace members wire each branch to a different downstream step. Return different id values for different code paths to build multi-branch flows. The Outcome component declares each id to the editor and describes what data that branch carries. The editor uses this to expose typed variables to downstream steps.
<>
  <Outcome
    id="created"
    label="Task created"
    schema={{
      task_id: Workflows.OutcomeSchema.string(),
      task_url: Workflows.OutcomeSchema.string(),
    }}
  />
  <Outcome id="not-found" label="Project not found" schema={null} />
</>
PropTypeDescription
idstringMatches the id returned by your handler
labelstringHuman-readable name shown in the editor as the branch label
schemaobject | nullShape of the outcome data, built with Workflows.OutcomeSchema.* nodes. Pass null for outcomes with no data.
When the block has only one outcome, label is optional; the editor uses the block’s title as the branch label. When the block has multiple outcomes, always provide label so workspace members can tell the branches apart.
The schema prop uses Workflows.OutcomeSchema.* constructors, a separate namespace from the config schema constructors. Available types mirror the Outcome schema node types.
Workflows.OutcomeSchema.string()
Workflows.OutcomeSchema.number()
Workflows.OutcomeSchema.boolean()
Workflows.OutcomeSchema.date()
Workflows.OutcomeSchema.timestamp()
Workflows.OutcomeSchema.emailAddress()
Workflows.OutcomeSchema.array(Workflows.OutcomeSchema.string())
Workflows.OutcomeSchema.struct({key: Workflows.OutcomeSchema.string()})
// ...and all other outcome schema node types

Watching config values

watch reads the current value of a config field as the workspace member types in the editor. Use it to conditionally show or hide other inputs based on what has been filled in. watch returns a discriminated union; always check type before reading value. value is fully typed — on a ConfigSchema.stringEnum(["basic", "advanced"]) field, modeConfig.value is "basic" | "advanced", not string. On trigger blocks, type can only ever be "static": dynamic variable references come from previous steps, and a trigger has none.
ShapeMeaning
undefinedField not yet filled in
{type: "static", value: T}User entered a literal value; value is typed against the schema field
{type: "dynamic"}User wired a variable reference (e.g. output from a previous step); value is not known at config time
export default Workflows.defineConfigurator(block, (workflowBlock) => {
  const {ComboboxInput, TextInput, NumberInput, Outcome, watch} =
    Workflows.useConfigurator(workflowBlock)

  // "mode" is defined as ConfigSchema.stringEnum(["basic", "advanced"]) in the block
  const modeConfig = watch("mode") // undefined if not yet filled in

  // when type is "static", .value is typed as "basic" | "advanced" — not just string
  const mode = modeConfig?.type === "static" ? modeConfig.value : undefined

  return (
    <>
      <ComboboxInput name="mode" label="Mode" />
      {mode === "advanced" && <TextInput name="custom_endpoint" label="Custom endpoint" />}
      {mode === "advanced" && <NumberInput name="timeout_ms" label="Timeout (ms)" />}
      <Outcome id="done" schema={null} />
    </>
  )
})
type: "dynamic" means the field will be resolved at runtime, not in the editor. You cannot read a concrete value from it. When a field is dynamic, either show all dependent inputs (safe default) or hide inputs that require a known value.

Example

A complete step configurator with two outcomes and typed data:
configurator.tsx
import {Workflows} from "attio/client"
import block from "./block"

export default Workflows.defineConfigurator(block, (workflowBlock) => {
  const {TextInput, DateInput, Outcome} = Workflows.useConfigurator(workflowBlock)
  return (
    <>
      <TextInput name="project_id" label="Project ID" />
      <TextInput name="content" label="Task name" />
      <DateInput name="due_date" label="Due date" />
      <Outcome
        id="created"
        label="Task created"
        schema={{
          task_id: Workflows.OutcomeSchema.string(),
          task_url: Workflows.OutcomeSchema.string(),
        }}
      />
      <Outcome id="project-not-found" label="Project not found" schema={null} />
    </>
  )
})

See also