---
title: "Replace a workflow's step graph"
method: PUT
path: "/api/v1/workflows/{id}/graph"
tags: ["Workflows"]
---

# Replace a workflow's step graph

`PUT /api/v1/workflows/{id}/graph`

## Request

- Base URL: `https://api.sendly.now`
- URL: `https://api.sendly.now/api/v1/workflows/{id}/graph`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `better-auth.session_token`
  - OAuth 2 (authorization code, authorization URL `https://app.sendly.now/api/auth/oauth2/authorize`, token URL `https://app.sendly.now/api/auth/oauth2/token`, scopes: `workflows:write`)

Replaces the whole graph in one transaction, because a graph is only meaningful as nodes PLUS the edges between them — an edit that could apply half of it would leave steps pointing at steps that no longer exist.

A step whose id you send is kept and updated in place; a fresh uuid creates one; an id you omit deletes that step and its run history. Exactly one step must be a `TRIGGER`, every transition must name steps present in the same document, and a step may not point at itself.

Refused with 409 while the workflow has running executions: those runs are standing on the steps being replaced. Pause the workflow (`POST /api/v1/workflows/{id}/pause`) first.

The response of `GET /api/v1/workflows/{id}/graph` is accepted verbatim by `PUT` on the same path: read a graph, change a step, send it back. Step and transition ids are yours to choose on a write, which is what makes that round trip a no-op rather than a rebuild.

Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.

## Path parameters

- `id` string, uuid, required: Workflow id.

## Request body

- WorkflowGraphReplaceV1: Body for `PUT /api/v1/workflows/{id}/graph`. Replaces the whole graph: a step whose id you send is kept and updated, a fresh id is created, and a step you omit is deleted along with its run history. Refused with 409 while the workflow has running executions, because those runs are standing on the steps being replaced.
  - `steps` WorkflowStepV1[], required: The complete step set. Exactly one must be a `TRIGGER`.
    - union: One node of a workflow graph.
      - WorkflowTriggerStepV1: The graph's single entry node. Its config mirrors the workflow's own trigger: `eventName` for `EVENT`, `intervalMs` for `SCHEDULE`, empty for `MANUAL`.
        - `config` object, required
          - `eventName` string
          - `intervalMs` integer
        - `id` string, uuid, required: Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
        - `name` string, required
        - `position` WorkflowStepPositionV1, required: Where this step sits on the editor canvas.
          - `x` number, required
          - `y` number, required
        - `template_id` string, uuid, nullable: The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
        - `type` 'TRIGGER', required
      - WorkflowSendEmailStepV1: Sends one email to the contact. Give it either `template_id` (preferred) or an inline `subject` + `body`.
        - `config` object, required
          - `body` string
          - `recipient` object
            - `customEmail` string, email
            - `type` 'CONTACT' | 'CUSTOM', required
          - `subject` string
          - `templateId` string, uuid
        - `id` string, uuid, required: Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
        - `name` string, required
        - `position` WorkflowStepPositionV1, required: Where this step sits on the editor canvas.
          - `x` number, required
          - `y` number, required
        - `template_id` string, uuid, nullable: The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
        - `type` 'SEND_EMAIL', required
      - WorkflowDelayStepV1: Pauses the run for `amount` × `unit`, up to 365 days.
        - `config` object, required
          - `amount` number
          - `unit` 'minutes' | 'hours' | 'days'
        - `id` string, uuid, required: Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
        - `name` string, required
        - `position` WorkflowStepPositionV1, required: Where this step sits on the editor canvas.
          - `x` number, required
          - `y` number, required
        - `template_id` string, uuid, nullable: The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
        - `type` 'DELAY', required
      - WorkflowWaitForEventStepV1: Parks the run until `eventName` is recorded for this contact, or `timeout` seconds pass.
        - `config` object, required
          - `eventName` string
          - `timeout` number
        - `id` string, uuid, required: Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
        - `name` string, required
        - `position` WorkflowStepPositionV1, required: Where this step sits on the editor canvas.
          - `x` number, required
          - `y` number, required
        - `template_id` string, uuid, nullable: The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
        - `type` 'WAIT_FOR_EVENT', required
      - WorkflowConditionStepV1: Branches the run. Binary form: `field` + `operator` + `value`, whose outgoing transitions carry `{ "branch": "yes" }` / `{ "branch": "no" }`. Multi form: `mode: "multi"` + `field` + `branches`, whose transitions carry the branch id.
        - `config` object, required
          - `branches` object[]
            - `id` string, required
            - `name` string, required
            - `operator` 'equals' | 'notEquals' | 'contains' | 'notContains' | 'greaterThan' | 'lessThan' | 'greaterThanOrEqual' | 'lessThanOrEqual' | 'exists' | 'notExists', required
            - `value` union: Arbitrary JSON value (string, number, boolean, null, array, or object).
              - …
          - `field` string
          - `mode` 'multi'
          - `operator` 'equals' | 'notEquals' | 'contains' | 'notContains' | 'greaterThan' | 'lessThan' | 'greaterThanOrEqual' | 'lessThanOrEqual' | 'exists' | 'notExists'
          - `value` union: Arbitrary JSON value (string, number, boolean, null, array, or object).
            - string
            - number
            - boolean
            - object
        - `id` string, uuid, required: Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
        - `name` string, required
        - `position` WorkflowStepPositionV1, required: Where this step sits on the editor canvas.
          - `x` number, required
          - `y` number, required
        - `template_id` string, uuid, nullable: The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
        - `type` 'CONDITION', required
      - WorkflowExitStepV1: Ends the run early and stamps `exit_reason`.
        - `config` object, required
          - `reason` string
        - `id` string, uuid, required: Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
        - `name` string, required
        - `position` WorkflowStepPositionV1, required: Where this step sits on the editor canvas.
          - `x` number, required
          - `y` number, required
        - `template_id` string, uuid, nullable: The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
        - `type` 'EXIT', required
      - WorkflowWebhookStepV1: Calls an external URL. `url`, header values and the JSON body's string leaves are `{{variable}}`-interpolated from the contact and the run's variables.
        - `config` object, required
          - `body` union: Arbitrary JSON value (string, number, boolean, null, array, or object).
            - string
            - number
            - boolean
            - object
          - `headers` object
          - `method` 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
          - `url` string, uri
        - `id` string, uuid, required: Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
        - `name` string, required
        - `position` WorkflowStepPositionV1, required: Where this step sits on the editor canvas.
          - `x` number, required
          - `y` number, required
        - `template_id` string, uuid, nullable: The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
        - `type` 'WEBHOOK', required
      - WorkflowUpdateContactStepV1: Writes `updates` onto the contact, and optionally flips `subscribed`.
        - `config` object, required
          - `subscribed` boolean
          - `updates` object
        - `id` string, uuid, required: Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
        - `name` string, required
        - `position` WorkflowStepPositionV1, required: Where this step sits on the editor canvas.
          - `x` number, required
          - `y` number, required
        - `template_id` string, uuid, nullable: The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
        - `type` 'UPDATE_CONTACT', required
      - WorkflowSendAtOptimalTimeStepV1: Like `SEND_EMAIL`, but held until this contact's historically best open hour, falling back to `fallbackHour` and never waiting longer than `maxDelayHours`.
        - `config` object, required
          - `fallbackHour` integer
          - `maxDelayHours` number
          - `templateId` string, uuid
        - `id` string, uuid, required: Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
        - `name` string, required
        - `position` WorkflowStepPositionV1, required: Where this step sits on the editor canvas.
          - `x` number, required
          - `y` number, required
        - `template_id` string, uuid, nullable: The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
        - `type` 'SEND_AT_OPTIMAL_TIME', required
  - `transitions` WorkflowTransitionV1[], required: The complete edge set. Every `from_step_id`/`to_step_id` must name a step in this same document, and a step may not point at itself.
    - `condition` union, required: Null to always follow this edge. From a `CONDITION` step, `{ "branch": "yes" }`, `{ "branch": "no" }`, or `{ "branch": "<branch id>" }` in the multi form.
      - string
      - number
      - boolean
      - object
    - `from_step_id` string, uuid, required
    - `id` string, uuid, required: Caller-chosen on a write, exactly like a step id.
    - `priority` integer, required: Evaluation order among the edges leaving one step; lowest first.
    - `to_step_id` string, uuid, required

## Response `200`

The graph as it now stands

- WorkflowGraphV1: A workflow's complete step graph. The response of `GET /api/v1/workflows/{id}/graph` is accepted verbatim by `PUT` on the same path.
  - `steps` WorkflowStepReadV1[], required
    - `config` object, required: The step's configuration, exactly as stored. See `WorkflowStepV1` for the keys each step type uses; keys are camelCase.
    - `id` string, uuid, required: Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
    - `name` string, required
    - `position` WorkflowStepPositionV1, required: Where this step sits on the editor canvas.
      - `x` number, required
      - `y` number, required
    - `template_id` string, uuid, nullable: The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
    - `type` 'TRIGGER' | 'SEND_EMAIL' | 'DELAY' | 'WAIT_FOR_EVENT' | 'CONDITION' | 'EXIT' | 'WEBHOOK' | 'UPDATE_CONTACT' | 'SEND_AT_OPTIMAL_TIME', required
  - `transitions` WorkflowTransitionV1[], required
    - `condition` union, required: Null to always follow this edge. From a `CONDITION` step, `{ "branch": "yes" }`, `{ "branch": "no" }`, or `{ "branch": "<branch id>" }` in the multi form.
      - string
      - number
      - boolean
      - object
    - `from_step_id` string, uuid, required
    - `id` string, uuid, required: Caller-chosen on a write, exactly like a step id.
    - `priority` integer, required: Evaluation order among the edges leaving one step; lowest first.
    - `to_step_id` string, uuid, required
  - `version` integer, required: The workflow's version AFTER this read. Every structural change bumps it and writes a `workflow_versions` snapshot, so a changed number between two reads means somebody edited the graph.
  - `workflow_id` string, uuid, required

## Other responses

- `401`: `invalid_api_key` or `invalid_session` — missing or invalid credentials.
- `403`: `scope_missing`, `project_access_denied`, or `project_disabled`.
- `404`: `resource_not_found` — no workflow with this id belongs to the authenticated project.
- `409`: `conflict` — the workflow has running executions, or an id in the document already belongs to a different workflow.
- `422`: `validation_error` — query, path, or body parameters did not match the schema.
- `429`: `rate_limited` — see `Retry-After` and the `RateLimit` headers.
- `500`: `internal_error`.

---

[API](https://skmtc.dev/sendly/apis/sendly-api.md) · [All operations](https://skmtc.dev/sendly/apis/sendly-api/llms.txt) · [OpenAPI document](https://skmtc.dev/sendly/apis/sendly-api/revisions/ce7cfb7f7c7e?raw)
