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:
- Render inputs for each field in the block’s config schema so workspace members can fill them in.
- 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.
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.
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.
| Component | Schema type | Description |
|---|
TextInput | ConfigSchema.string() | Single-line text |
RichTextInput | ConfigSchema.richText() | Formatted text |
NumberInput | ConfigSchema.number() | Numeric value |
CheckboxInput | ConfigSchema.boolean() | True/false toggle |
ComboboxInput | ConfigSchema.stringEnum(values) | Dropdown from a fixed list |
DateInput | ConfigSchema.date() | Calendar date |
TimestampInput | ConfigSchema.timestamp() | Date and time |
DurationInput | ConfigSchema.duration() | Length of time |
EmailAddressInput | ConfigSchema.emailAddress() | Email address |
PhoneNumberInput | ConfigSchema.phoneNumber() | Phone number |
PersonalNameInput | ConfigSchema.personalName() | First and last name |
DomainInput | ConfigSchema.domain() | Web domain |
LocationInput | ConfigSchema.location() | Geographic location |
CurrencyInput | ConfigSchema.currency() | Monetary value |
AttioRecordInput | ConfigSchema.attioRecord() | Attio record |
AttioObjectInput | ConfigSchema.attioObject() | Attio object type |
AttioListInput | ConfigSchema.attioList() | Attio list |
AttioActorInput | ConfigSchema.attioActor() | Attio actor (user or workspace member) |
AttioAttributeInput | ConfigSchema.attioAttribute() | Attio attribute |
AttioSelectInput | ConfigSchema.attioSelect() | Select value from an Attio attribute |
AttioSequenceInput | ConfigSchema.attioSequence() | Attio sequence |
CollectionInput | ConfigSchema.array(element) | Repeating list of values |
All input components accept at minimum:
| Prop | Type | Required | Description |
|---|
name | string | Yes | Path to the schema field, type-checked against the schema |
label | string | Yes | Human-readable label shown above the input |
help | string | No | Help text shown below the input |
tooltip | string | No | Tooltip shown on hover |
placeholder | string | No | Placeholder 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} />
</>
| Prop | Type | Description |
|---|
id | string | Matches the id returned by your handler |
label | string | Human-readable name shown in the editor as the branch label |
schema | object | null | Shape 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.
| Shape | Meaning |
|---|
undefined | Field 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:
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