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

# Block lifecycle

> How Attio calls your workflow block handlers across a run

Understanding when each handler is called helps you reason about state, idempotency, and failure recovery. The lifecycle differs significantly between trigger and step blocks.

## Minimal example

* **Step block**: `execute.ts` calls an external API and returns an outcome:

  ```ts execute.ts theme={"system"}
  import {Workflows} from "attio/server"
  import block from "./block"

  export default Workflows.defineWorkflowBlockExecute(block, async ({config, metadata}) => {
    const result = await fetch("https://api.example.com/tasks", {
      method: "POST",
      body: JSON.stringify({title: config.task_title}),
    }).then((r) => r.json())

    return {type: "outcome", id: "created", data: {task_id: result.id}}
  })
  ```

* **Trigger block**: `activate.ts` registers the webhook, `trigger.ts` fires the run:

  ```ts activate.ts theme={"system"}
  import {Workflows} from "attio/server"
  import block from "./block"

  export default Workflows.defineWorkflowBlockActivate(block, async ({config, metadata}) => {
    await fetch("https://api.example.com/webhooks", {
      method: "POST",
      body: JSON.stringify({url: metadata.triggerCallbackUrl, id: metadata.uniqueActivationId}),
    })
    return {type: "complete"}
  })
  ```

  ```ts trigger.ts theme={"system"}
  import {Workflows} from "attio/server"
  import block from "./block"

  export default Workflows.defineWorkflowBlockTrigger(block, async (req, {config, metadata}) => {
    const payload = await req.json()
    return {type: "outcome", id: "received", data: {event_id: payload.id}}
  })
  ```

## Trigger lifecycle

A trigger manages an external subscription. Attio drives three distinct phases:

```mermaid theme={"system"}
sequenceDiagram
    participant A as Attio
    participant B as Your block
    participant E as External service

    Note over A: Workflow enabled
    A->>B: activate()
    B->>E: register triggerCallbackUrl
    alt success
        B->>A: {type: "complete"}
        Note over A: Workflow active
    else error
        B->>A: {type: "error"}
        Note over A: Not enabled — deactivate() never called
    end

    loop for each incoming event
        E->>A: POST triggerCallbackUrl
        A->>B: trigger()
        alt start a run
            B->>A: {type: "outcome"}
            Note over A: New run started
        else ignore
            B->>A: {type: "no-op"}
        end
    end

    Note over A: Workflow disabled
    A->>B: deactivate()
    B->>E: remove triggerCallbackUrl registration
    B->>A: {type: "complete"}
    Note over A: Workflow inactive
```

**Key points:**

* `activate` and `deactivate` each run **once** per workflow version. When a workspace member edits and re-enables the workflow, Attio deactivates the old version and activates the new one. You may receive events for the old `triggerCallbackUrl` during this window.
* `trigger` runs **once per incoming request** to `triggerCallbackUrl`. It should be fast and side-effect-free beyond deciding whether to start a run. Any heavy work belongs in a step block.
* `uniqueActivationId` is stable across retries of the same `activate` call. Store it alongside your webhook registration so you can identify and clean it up in `deactivate`.
* If `activate` returns `{type: "error"}`, the workflow is not enabled and `deactivate` is never called.

## Step lifecycle

A step runs inline during a workflow run. The basic path is a single call to `execute`:

```mermaid theme={"system"}
flowchart LR
    A([Run reaches block]) --> B["execute()"]
    B -->|outcome| C([Run continues])
    B -->|exit| D([Run stops])
    B -->|error| E([Run errors])
    B -->|defer| F([Block paused])
    F -->|"POST finishCallbackUrl"| G["finish()"]
    G -->|outcome| C
    G -->|exit| D
    G -->|error| E
    G -->|no-op| F
```

**Key points:**

* `uniqueExecutionId` is stable across retries of the same `execute` call. Use it as an idempotency key when calling external APIs to avoid duplicate side-effects.
* Returning `{type: "defer"}` suspends the block; the run pauses until an external service POSTs to `finishCallbackUrl`. No server resources are held while waiting.
* `finish` is only required when `execute` may return `{type: "defer"}`. If `execute` never defers, the file is optional.
* A deferred block can receive multiple callbacks before resolving; return `{type: "no-op"}` to stay deferred and wait for another POST.
* `retryable: true` on an `{type: "error"}` return tells Attio it may safely retry the execution. Only set this when the operation is genuinely idempotent.

### Defer

Use `defer` when your block must wait for something that happens outside the current HTTP request: a human approval, a slow background job, or a webhook from an external system. Unlike returning an outcome immediately, `defer` tells Attio to suspend the run entirely until an external signal arrives.

```mermaid theme={"system"}
sequenceDiagram
    participant A as Attio
    participant B as Your block
    participant E as External service

    A->>B: execute()
    B->>E: register finishCallbackUrl
    B->>A: {type: "defer"}
    Note over A: Run suspended — no resources held

    loop until resolved
        E->>A: POST finishCallbackUrl
        A->>B: finish()
        alt still waiting
            B->>A: {type: "no-op"}
        else done
            B->>A: outcome / exit / error
        end
    end

    A->>A: Run continues (or stops)
```

**The flow in detail:**

1. **`execute()` runs.** Before returning, register `metadata.finishCallbackUrl` with the external service; this is the URL it will POST to when the work is done.
2. **Return `{type: "defer"}`**. Attio immediately suspends the block. The workflow run is checkpointed and paused; no server resources are held while waiting. There is no timeout; the block can stay deferred indefinitely.
3. **External event fires.** The external service POSTs to `finishCallbackUrl` with any payload it wants.
4. **Attio calls `finish()`** with the incoming request. The handler reads the payload and decides what to do.
5. **Resolve or stay deferred.** Return an outcome / exit / error to resume the run, or return `{type: "no-op"}` to stay deferred and wait for the next POST. The external service can POST multiple times before the block resolves, which is useful for polling-style callbacks or multi-step approvals.

<Note>
  `finishCallbackUrl` is unique per execution. Register it with the external service inside `execute()`, not before. Do not share it across runs or blocks.
</Note>

```ts execute.ts theme={"system"}
import {Workflows} from "attio/server"
import block from "./block"

export default Workflows.defineWorkflowBlockExecute(block, async ({config, metadata}) => {
  // Register the callback URL — the external service will POST here when done
  await fetch("https://api.example.com/jobs", {
    method: "POST",
    body: JSON.stringify({
      callback_url: metadata.finishCallbackUrl,
      task: config.task_title,
    }),
  })

  // Suspend the run — Attio calls finish() when the job POSTs back
  return {type: "defer"}
})
```

```ts finish.ts theme={"system"}
import {Workflows} from "attio/server"
import block from "./block"

export default Workflows.defineWorkflowBlockFinish(block, async (req, {config, metadata}) => {
  const payload = await req.json()

  if (payload.status === "pending") {
    // Job not finished — stay deferred and wait for the next POST
    return {type: "no-op"}
  }

  return {type: "outcome", id: "completed", data: {result: payload.result}}
})
```

## Configurator

`configurator.tsx` is **not** part of the runtime lifecycle. It runs in the browser, inside the workflow editor, when a workspace member is configuring the block. It has no access to server-side APIs and should never perform side-effects. See [Configurator](./configurator).

## See also

* [Registering a trigger](./define-workflow-block-activate): trigger activate handler
* [Receiving a trigger event](./define-workflow-block-on-trigger-callback): trigger event handler
* [Deactivating a trigger](./define-workflow-block-deactivate): trigger deactivate handler
* [Executing a step](./define-workflow-block-execute): step execute handler
* [Finishing a deferred step](./define-workflow-block-resume-callback): deferred step handler
