> ## Documentation Index
> Fetch the complete documentation index at: https://docs.attio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Configurator

> Render config inputs and declare outcomes in the workflow editor

`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.

```tsx configurator.tsx theme={"system"}
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.

| 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)` or `ConfigSchema.string()` | Dropdown — a fixed list, or searchable options you supply |
| `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 |

### Combobox options

A `ComboboxInput` on a `ConfigSchema.string()` field needs an `options` prop that supplies the choices. Pass either a static array of `{value, label}` options:

```tsx theme={"system"}
<ComboboxInput
  name="priority"
  label="Priority"
  options={[
    {value: "low", label: "Low"},
    {value: "high", label: "High"},
  ]}
/>
```

Or an **options provider** that loads them from a third party as the member types. A provider is an object with `search` (returns options matching a query) and `getOption` (resolves a single saved value back to its label). Both call [server functions](/sdk/server/server-functions), since the configurator cannot `fetch` directly.

```ts project-options.ts theme={"system"}
import type {PlainComboboxOptionsProvider} from "attio/client"
import getProject from "./get-project.server"
import listProjects from "./list-projects.server"

export const projectOptions: PlainComboboxOptionsProvider = {
  async search(query) {
    const projects = await listProjects(query)
    return projects.map(({id, name}) => ({value: id, label: name}))
  },
  async getOption(value) {
    const project = await getProject(value)
    return project ? {label: project.name} : undefined
  },
}
```

```tsx theme={"system"}
<ComboboxInput name="project_id" label="Project" options={projectOptions} />
```

See [`<Combobox />`](/sdk/components/forms/combobox) for the full options-provider reference.

## 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.

```tsx theme={"system"}
<>
  <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. |

<Note>
  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.
</Note>

The `schema` prop uses `Workflows.OutcomeSchema.*` constructors, a separate namespace from the config schema constructors. Available types mirror the [Outcome schema](./outcome-schema) node types.

```ts theme={"system"}
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 |

```tsx theme={"system"}
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} />
    </>
  )
})
```

<Note>
  `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.
</Note>

## Loading data

Configurators often need data from a third-party service — the list of projects to pick from, the fields on a selected record, and so on. Declare your `Outcome`s up front, independent of anything you load, and let the block suspend while data is loading: the editor keeps the block in a loading state until it stops suspending, then reads its outcomes.

### Prefer an options provider for dropdowns

Load combobox choices with an [options provider](#combobox-options). The provider fetches options lazily as the member searches, so the block renders right away and your outcomes stay fixed.

```tsx configurator.tsx theme={"system"}
import {Workflows} from "attio/client"
import {projectOptions} from "./project-options"
import block from "./block"

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

### Loading other data with useAsyncCache

When you need data that isn't a set of combobox options — for example, to label or shape the inputs you render — load it with [`useAsyncCache`](/sdk/data-fetching/use-async-cache). It suspends while loading, so the editor keeps the block in its loading state until the data is ready; you don't need to add your own loading indicator.

Use a fixed cache key so it loads once when the configurator opens.

```tsx configurator.tsx theme={"system"}
import {useAsyncCache, Workflows} from "attio/client"
import loadAccount from "./load-account.server"
import block from "./block"

// Loads once when the configurator opens.
function TaskNameInput() {
  const {TextInput} = Workflows.useConfigurator(block)
  const {values} = useAsyncCache({account: loadAccount})
  return (
    <TextInput
      name="content"
      label="Task name"
      help={`Adding to the ${values.account.workspace_name} workspace.`}
    />
  )
}

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

## Example

A complete step configurator with two outcomes and typed data:

```tsx configurator.tsx theme={"system"}
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

* [Config schema](./config-schema): all available schema field types
* [Outcome schema](./outcome-schema): typing the data field in return values
* [Block definition](./define-workflow-block): block identity and config schema
* [useAsyncCache](/sdk/data-fetching/use-async-cache): load data inside the configurator
* [`<Combobox />`](/sdk/components/forms/combobox): full options-provider reference
* [Building workflow blocks](/guides/building-workflow-blocks): full working examples


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.