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

# Workflows API examples

> Copy/paste code examples for listing, creating, and editing workflows and nodes via the Loops API, SDKs, and CLI.

## List workflows

Retrieve a paginated list of workflows, most recently updated first.

[API reference](https://loops.so/docs/api-reference/list-workflows.md)\
[CLI reference](https://loops.so/docs/cli/workflows.md#list)

```bash CLI theme={"dark"}
loops workflows list -o json
```

```js JavaScript theme={"dark"}
const response = await fetch("https://app.loops.so/api/v1/workflows?perPage=20", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <your-api-key>",
  },
});

const { data, pagination } = await response.json();
const workflowId = data[0]?.id;
const nextCursor = pagination.nextCursor;
```

```js JavaScript SDK theme={"dark"}
import { LoopsClient } from "loops";

const loops = new LoopsClient("<your-api-key>");

const { data, pagination } = await loops.listWorkflows({ perPage: 20 });
const workflowId = data[0]?.id;
const nextCursor = pagination.nextCursor;
```

```php PHP SDK theme={"dark"}
use Loops\LoopsClient;

$loops = new LoopsClient('<your-api-key>');

$result = $loops->workflows->list(per_page: 20);
$workflow_id = $result['data'][0]['id'] ?? null;
$next_cursor = $result['pagination']['nextCursor'];
```

```ruby Ruby SDK theme={"dark"}
response = LoopsSdk::Workflows.list(perPage: 20)

workflow_id = response["data"][0]&.dig("id")
next_cursor = response["pagination"]["nextCursor"]
```

```python Python theme={"dark"}
import requests

response = requests.get(
    "https://app.loops.so/api/v1/workflows",
    headers={
        "Authorization": "Bearer <your-api-key>",
    },
    params={"perPage": 20},
)

body = response.json()
workflows = body["data"]
workflow_id = workflows[0]["id"] if workflows else None
next_cursor = body["pagination"]["nextCursor"]
```

## Get a workflow

Fetch a simplified workflow graph with node types, connections, and selected
display fields.

> [!TIP]
>
> Save `rootNodeId` and the keys in `nodes`. Use them to request full node
> details with [Get workflow node](https://loops.so/docs/api-reference/get-workflow-node.md).

[API reference](https://loops.so/docs/api-reference/get-workflow.md)\
[CLI reference](https://loops.so/docs/cli/workflows.md#get)

```bash CLI theme={"dark"}
loops workflows get $workflowId -o json
```

```js JavaScript theme={"dark"}
const response = await fetch(
  `https://app.loops.so/api/v1/workflows/${workflowId}`,
  {
    method: "GET",
    headers: {
      "Authorization": "Bearer <your-api-key>",
    },
  },
);

const workflow = await response.json();
const rootNodeId = workflow.rootNodeId;
const nodes = workflow.nodes;
const firstNode = nodes[rootNodeId];
const nextNodeIds = firstNode?.nextNodeIds ?? [];
```

```js JavaScript SDK theme={"dark"}
import { LoopsClient } from "loops";

const loops = new LoopsClient("<your-api-key>");

const workflow = await loops.getWorkflow(workflowId);
const rootNodeId = workflow.rootNodeId;
const nodes = workflow.nodes;
const firstNode = nodes[rootNodeId];
const nextNodeIds = firstNode?.nextNodeIds ?? [];
```

```php PHP SDK theme={"dark"}
use Loops\LoopsClient;

$loops = new LoopsClient('<your-api-key>');

$workflow = $loops->workflows->get(workflow_id: $workflow_id);
$root_node_id = $workflow['rootNodeId'];
$nodes = $workflow['nodes'];
$first_node = $nodes[$root_node_id];
$next_node_ids = $first_node['nextNodeIds'] ?? [];
```

```ruby Ruby SDK theme={"dark"}
workflow = LoopsSdk::Workflows.get(workflow_id: workflow_id)

root_node_id = workflow["rootNodeId"]
nodes = workflow["nodes"]
first_node = nodes[root_node_id]
next_node_ids = first_node&.dig("nextNodeIds") || []
```

```python Python theme={"dark"}
response = requests.get(
    f"https://app.loops.so/api/v1/workflows/{workflow_id}",
    headers={
        "Authorization": "Bearer <your-api-key>",
    },
)

workflow = response.json()
root_node_id = workflow["rootNodeId"]
nodes = workflow["nodes"]
first_node = nodes.get(root_node_id)
next_node_ids = first_node.get("nextNodeIds", []) if first_node else []
```

## Get workflow node details

Use the simplified workflow graph to pick a `nodeId`, then fetch the full node
configuration.

[API reference](https://loops.so/docs/api-reference/get-workflow-node.md)\
[CLI reference](https://loops.so/docs/cli/workflows.md#nodes-get)

```bash CLI theme={"dark"}
loops workflows nodes get $workflowId $nodeId -o json
```

```js JavaScript theme={"dark"}
const nodeId = rootNodeId;

const response = await fetch(
  `https://app.loops.so/api/v1/workflows/${workflowId}/nodes/${nodeId}`,
  {
    method: "GET",
    headers: {
      "Authorization": "Bearer <your-api-key>",
    },
  },
);

const node = await response.json();

if (node.typeName === "EventTrigger") {
  console.log(node.eventName, node.reEligible);
}

if (node.typeName === "SendEmailAction") {
  // Use emailMessageId to read or edit the email's content (see below).
  console.log(node.emailMessageId, node.subject);
}
```

```js JavaScript SDK theme={"dark"}
import { LoopsClient } from "loops";

const loops = new LoopsClient("<your-api-key>");

const nodeId = rootNodeId;

const node = await loops.getWorkflowNode(workflowId, nodeId);

if (node.typeName === "EventTrigger") {
  console.log(node.eventName, node.reEligible);
}

if (node.typeName === "SendEmailAction") {
  console.log(node.subject);
}
```

```php PHP SDK theme={"dark"}
use Loops\LoopsClient;

$loops = new LoopsClient('<your-api-key>');

$node_id = $root_node_id;

$node = $loops->workflows->getNode(
  workflow_id: $workflow_id,
  node_id: $node_id,
);

if ($node['typeName'] === 'EventTrigger') {
  echo $node['eventName'], $node['reEligible'];
}

if ($node['typeName'] === 'SendEmailAction') {
  echo $node['emailMessageId'], $node['subject'];
}
```

```ruby Ruby SDK theme={"dark"}
node_id = root_node_id

node = LoopsSdk::Workflows.get_node(
  workflow_id: workflow_id,
  node_id: node_id,
)

if node["typeName"] == "EventTrigger"
  puts node["eventName"], node["reEligible"]
end

if node["typeName"] == "SendEmailAction"
  puts node["subject"]
end
```

```python Python theme={"dark"}
node_id = root_node_id

response = requests.get(
    f"https://app.loops.so/api/v1/workflows/{workflow_id}/nodes/{node_id}",
    headers={
        "Authorization": "Bearer <your-api-key>",
    },
)

node = response.json()

if node["typeName"] == "EventTrigger":
    print(node.get("eventName"), node.get("reEligible"))

if node["typeName"] == "SendEmailAction":
    # Use emailMessageId to read or edit the email's content (see below).
    print(node.get("emailMessageId"), node.get("subject"))
```

## Walk connected nodes

After fetching a node, follow `nextNodeIds` to load the next steps in the
workflow.

```bash CLI theme={"dark"}
for nodeId in $nextNodeIds; do
  loops workflows nodes get $workflowId $nodeId -o json
done
```

```js JavaScript theme={"dark"}
async function getWorkflowNodes(workflowId, nodeIds) {
  const nodes = await Promise.all(
    nodeIds.map(async (nodeId) => {
      const response = await fetch(
        `https://app.loops.so/api/v1/workflows/${workflowId}/nodes/${nodeId}`,
        {
          method: "GET",
          headers: {
            "Authorization": "Bearer <your-api-key>",
          },
        },
      );

      return response.json();
    }),
  );

  return nodes;
}

const connectedNodes = await getWorkflowNodes(workflowId, nextNodeIds);
```

```js JavaScript SDK theme={"dark"}
import { LoopsClient } from "loops";

const loops = new LoopsClient("<your-api-key>");

async function getWorkflowNodes(workflowId, nodeIds) {
  return Promise.all(
    nodeIds.map((nodeId) => loops.getWorkflowNode(workflowId, nodeId)),
  );
}

const connectedNodes = await getWorkflowNodes(workflowId, nextNodeIds);
```

```php PHP SDK theme={"dark"}
use Loops\LoopsClient;

$loops = new LoopsClient('<your-api-key>');

function get_workflow_nodes(LoopsClient $loops, string $workflow_id, array $node_ids): array {
  return array_map(
    fn (string $node_id) => $loops->workflows->getNode(
      workflow_id: $workflow_id,
      node_id: $node_id,
    ),
    $node_ids,
  );
}

$connected_nodes = get_workflow_nodes($loops, $workflow_id, $next_node_ids);
```

```ruby Ruby SDK theme={"dark"}
def get_workflow_nodes(workflow_id, node_ids)
  node_ids.map do |node_id|
    LoopsSdk::Workflows.get_node(
      workflow_id: workflow_id,
      node_id: node_id,
    )
  end
end

connected_nodes = get_workflow_nodes(workflow_id, next_node_ids)
```

```python Python theme={"dark"}
def get_workflow_nodes(workflow_id, node_ids):
    nodes = []

    for node_id in node_ids:
        response = requests.get(
            f"https://app.loops.so/api/v1/workflows/{workflow_id}/nodes/{node_id}",
            headers={
                "Authorization": "Bearer <your-api-key>",
            },
        )
        nodes.append(response.json())

    return nodes

connected_nodes = get_workflow_nodes(workflow_id, next_node_ids)
```

## Create a workflow with an event trigger

Create a draft workflow (blank trigger + exit), look up an [event pattern](https://loops.so/docs/api-reference/list-event-patterns.md), then configure the root trigger as an `EventTrigger`.

Create workflow [API reference](https://loops.so/docs/api-reference/create-workflow.md) [CLI reference](https://loops.so/docs/cli/workflows.md#create)\
Update node [API reference](https://loops.so/docs/api-reference/update-workflow-node.md) [CLI reference](https://loops.so/docs/cli/workflows.md#nodes-update)\
Get event pattern by name [API reference](https://loops.so/docs/api-reference/get-event-pattern-by-name.md) [CLI reference](https://loops.so/docs/cli/event-patterns.md#get)

```bash CLI theme={"dark"}
loops event-patterns get --name signupCompleted -o json

loops workflows create \
  --name "Post-signup nurture" \
  --description "Triggered when signupCompleted fires" \
  -o json > response.json

workflowRevisionId=$(jq -r '.workflowRevisionId' response.json)

# Use rootNodeId and workflowRevisionId from the create response.
cat > event-trigger.json <<'EOF'
{
  "typeName": "EventTrigger",
  "eventName": "signupCompleted",
  "reEligible": false
}
EOF

loops workflows nodes update $workflowId $rootNodeId \
  --payload-file event-trigger.json \
  --expected-revision-id $workflowRevisionId \
  -o json
```

```js JavaScript theme={"dark"}
const eventPattern = await fetch(
  "https://app.loops.so/api/v1/event-patterns/by-name/signupCompleted",
  {
    method: "GET",
    headers: {
      "Authorization": "Bearer <your-api-key>",
    },
  },
).then((response) => response.json());

const workflow = await fetch("https://app.loops.so/api/v1/workflows", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <your-api-key>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Post-signup nurture",
    description: "Triggered when signupCompleted fires",
  }),
}).then((response) => response.json());

const trigger = await fetch(
  `https://app.loops.so/api/v1/workflows/${workflow.id}/nodes/${workflow.rootNodeId}`,
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      expectedRevisionId: workflow.workflowRevisionId,
      payload: {
        typeName: "EventTrigger",
        eventName: eventPattern.eventName,
        reEligible: false,
      },
    }),
  },
).then((response) => response.json());

const workflowRevisionId = trigger.workflowRevisionId;
```

```js JavaScript SDK theme={"dark"}
import { LoopsClient } from "loops";

const loops = new LoopsClient("<your-api-key>");

const eventPattern = await loops.getEventPatternByName("signupCompleted");

const workflow = await loops.createWorkflow({
  name: "Post-signup nurture",
  description: "Triggered when signupCompleted fires",
});

const trigger = await loops.updateWorkflowNode(workflow.id, workflow.rootNodeId, {
  expectedRevisionId: workflow.workflowRevisionId,
  payload: {
    typeName: "EventTrigger",
    eventName: eventPattern.eventName,
    reEligible: false,
  },
});

const workflowRevisionId = trigger.workflowRevisionId;
```

```php PHP SDK theme={"dark"}
use Loops\LoopsClient;

$loops = new LoopsClient('<your-api-key>');

$event_pattern = $loops->eventPatterns->getByName(event_name: 'signupCompleted');

$workflow = $loops->workflows->create(
  name: 'Post-signup nurture',
  description: 'Triggered when signupCompleted fires',
);

$trigger = $loops->workflows->updateNode(
  workflow_id: $workflow['id'],
  node_id: $workflow['rootNodeId'],
  expected_revision_id: $workflow['workflowRevisionId'],
  payload: [
    'typeName' => 'EventTrigger',
    'eventName' => $event_pattern['eventName'],
    'reEligible' => false,
  ],
);

$workflow_revision_id = $trigger['workflowRevisionId'];
```

```ruby Ruby SDK theme={"dark"}
event_pattern = LoopsSdk::EventPatterns.get_by_name(event_name: "signupCompleted")

workflow = LoopsSdk::Workflows.create(
  name: "Post-signup nurture",
  description: "Triggered when signupCompleted fires",
)

trigger = LoopsSdk::Workflows.update_node(
  workflow_id: workflow["id"],
  node_id: workflow["rootNodeId"],
  expected_revision_id: workflow["workflowRevisionId"],
  payload: {
    typeName: "EventTrigger",
    eventName: event_pattern["eventName"],
    reEligible: false,
  },
)

workflow_revision_id = trigger["workflowRevisionId"]
```

```python Python theme={"dark"}
event_pattern = requests.get(
    "https://app.loops.so/api/v1/event-patterns/by-name/signupCompleted",
    headers={
        "Authorization": "Bearer <your-api-key>",
    },
).json()

workflow = requests.post(
    "https://app.loops.so/api/v1/workflows",
    headers={
        "Authorization": "Bearer <your-api-key>",
        "Content-Type": "application/json",
    },
    json={
        "name": "Post-signup nurture",
        "description": "Triggered when signupCompleted fires",
    },
).json()

trigger = requests.post(
    f"https://app.loops.so/api/v1/workflows/{workflow['id']}/nodes/{workflow['rootNodeId']}",
    headers={
        "Authorization": "Bearer <your-api-key>",
        "Content-Type": "application/json",
    },
    json={
        "expectedRevisionId": workflow["workflowRevisionId"],
        "payload": {
            "typeName": "EventTrigger",
            "eventName": event_pattern["eventName"],
            "reEligible": False,
        },
    },
).json()

workflow_revision_id = trigger["workflowRevisionId"]
```

## Change a trigger type

Switch a [Contact added](https://loops.so/docs/workflows/triggers.md#contact-added) (`SignupTrigger`) root node to [Contact added to list](https://loops.so/docs/workflows/triggers.md#contact-added-to-list) (`AddToListTrigger`). Trigger swaps are done with `typeName` on the update payload.

To set the trigger's mailing list, use the "change mailing list" endpoint.

Update node [API reference](https://loops.so/docs/api-reference/update-workflow-node.md) [CLI reference](https://loops.so/docs/cli/workflows.md#nodes-update)\
Change mailing list [API reference](https://loops.so/docs/api-reference/change-workflow-mailing-list.md) [CLI reference](https://loops.so/docs/cli/workflows.md#change-mailing-list)

```bash CLI theme={"dark"}
cat > add-to-list-trigger.json <<'EOF'
{
  "typeName": "AddToListTrigger",
  "reEligible": true
}
EOF

# Capture workflowRevisionId from this response before the next call.
loops workflows nodes update $workflowId $rootNodeId \
  --payload-file add-to-list-trigger.json \
  --expected-revision-id $workflowRevisionId \
  -o json > response.json

workflowRevisionId=$(jq -r '.workflowRevisionId' response.json)

loops workflows change-mailing-list $workflowId \
  --mailing-list-id $mailingListId \
  --expected-revision-id $workflowRevisionId \
  -o json
```

```js JavaScript theme={"dark"}
const trigger = await fetch(
  `https://app.loops.so/api/v1/workflows/${workflowId}/nodes/${rootNodeId}`,
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      expectedRevisionId: workflowRevisionId,
      payload: {
        typeName: "AddToListTrigger",
        reEligible: true,
      },
    }),
  },
).then((response) => response.json());

const mailingListResult = await fetch(
  `https://app.loops.so/api/v1/workflows/${workflowId}/mailing-list`,
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      expectedRevisionId: trigger.workflowRevisionId,
      mailingListId,
    }),
  },
).then((response) => response.json());

workflowRevisionId = mailingListResult.workflowRevisionId;
```

```js JavaScript SDK theme={"dark"}
import { LoopsClient } from "loops";

const loops = new LoopsClient("<your-api-key>");

const trigger = await loops.updateWorkflowNode(workflowId, rootNodeId, {
  expectedRevisionId: workflowRevisionId,
  payload: {
    typeName: "AddToListTrigger",
    reEligible: true,
  },
});

const mailingListResult = await loops.changeWorkflowMailingList(workflowId, {
  expectedRevisionId: trigger.workflowRevisionId,
  mailingListId,
});

workflowRevisionId = mailingListResult.workflowRevisionId;
```

```php PHP SDK theme={"dark"}
use Loops\LoopsClient;

$loops = new LoopsClient('<your-api-key>');

$trigger = $loops->workflows->updateNode(
  workflow_id: $workflow_id,
  node_id: $root_node_id,
  expected_revision_id: $workflow_revision_id,
  payload: [
    'typeName' => 'AddToListTrigger',
    'reEligible' => true,
  ],
);

$mailing_list_result = $loops->workflows->changeMailingList(
  workflow_id: $workflow_id,
  expected_revision_id: $trigger['workflowRevisionId'],
  mailing_list_id: $mailing_list_id,
);

$workflow_revision_id = $mailing_list_result['workflowRevisionId'];
```

```ruby Ruby SDK theme={"dark"}
trigger = LoopsSdk::Workflows.update_node(
  workflow_id: workflow_id,
  node_id: root_node_id,
  expected_revision_id: workflow_revision_id,
  payload: {
    typeName: "AddToListTrigger",
    reEligible: true,
  },
)

mailing_list_result = LoopsSdk::Workflows.change_mailing_list(
  workflow_id: workflow_id,
  expected_revision_id: trigger["workflowRevisionId"],
  mailing_list_id: mailing_list_id,
)

workflow_revision_id = mailing_list_result["workflowRevisionId"]
```

```python Python theme={"dark"}
trigger = requests.post(
    f"https://app.loops.so/api/v1/workflows/{workflow_id}/nodes/{root_node_id}",
    headers={
        "Authorization": "Bearer <your-api-key>",
        "Content-Type": "application/json",
    },
    json={
        "expectedRevisionId": workflow_revision_id,
        "payload": {
            "typeName": "AddToListTrigger",
            "reEligible": True,
        },
    },
).json()

mailing_list_result = requests.post(
    f"https://app.loops.so/api/v1/workflows/{workflow_id}/mailing-list",
    headers={
        "Authorization": "Bearer <your-api-key>",
        "Content-Type": "application/json",
    },
    json={
        "expectedRevisionId": trigger["workflowRevisionId"],
        "mailingListId": mailing_list_id,
    },
).json()

workflow_revision_id = mailing_list_result["workflowRevisionId"]
```

## Insert a timer and an email node

Insert nodes with `insertMode: "before"` (or `"between"`). New nodes start with defaults — configure the timer after creation. Email content is edited separately with the [email message APIs](#edit-a-workflow-emails-content).

Create node [API reference](https://loops.so/docs/api-reference/create-workflow-node.md) [CLI reference](https://loops.so/docs/cli/workflows.md#nodes-create)\
Update node [API reference](https://loops.so/docs/api-reference/update-workflow-node.md) [CLI reference](https://loops.so/docs/cli/workflows.md#nodes-update)

```bash CLI theme={"dark"}
# Graph starts as trigger -> exit. Insert timer before exit, then email before exit.
# Capture node.id and workflow.workflowRevisionId from each create response.
loops workflows nodes create $workflowId \
  --insert-mode before \
  --node-type TimerAction \
  --before-node-id $exitNodeId \
  --expected-revision-id $workflowRevisionId \
  -o json

cat > timer.json <<'EOF'
{
  "typeName": "TimerAction",
  "amount": 2,
  "unit": "d"
}
EOF

loops workflows nodes update $workflowId $timerNodeId \
  --payload-file timer.json \
  --expected-revision-id $workflowRevisionId \
  -o json > response.json

workflowRevisionId=$(jq -r '.workflowRevisionId' response.json)

loops workflows nodes create $workflowId \
  --insert-mode before \
  --node-type SendEmailAction \
  --before-node-id $exitNodeId \
  --expected-revision-id $workflowRevisionId \
  -o json
```

```js JavaScript theme={"dark"}
const exitNodeId = workflow.nodes[workflow.rootNodeId].nextNodeIds[0];

const timerCreated = await fetch(
  `https://app.loops.so/api/v1/workflows/${workflowId}/nodes`,
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      expectedRevisionId: workflowRevisionId,
      insertMode: "before",
      nodeTypeName: "TimerAction",
      toNodeId: exitNodeId,
    }),
  },
).then((response) => response.json());

const timerNodeId = timerCreated.node.id;
workflowRevisionId = timerCreated.workflow.workflowRevisionId;

const timer = await fetch(
  `https://app.loops.so/api/v1/workflows/${workflowId}/nodes/${timerNodeId}`,
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      expectedRevisionId: workflowRevisionId,
      payload: { amount: 2, unit: "d" },
    }),
  },
).then((response) => response.json());

workflowRevisionId = timer.workflowRevisionId;

const emailCreated = await fetch(
  `https://app.loops.so/api/v1/workflows/${workflowId}/nodes`,
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      expectedRevisionId: workflowRevisionId,
      insertMode: "before",
      nodeTypeName: "SendEmailAction",
      toNodeId: exitNodeId,
    }),
  },
).then((response) => response.json());

const emailMessageId = emailCreated.node.emailMessageId;
workflowRevisionId = emailCreated.workflow.workflowRevisionId;
```

```js JavaScript SDK theme={"dark"}
import { LoopsClient } from "loops";

const loops = new LoopsClient("<your-api-key>");

const exitNodeId = workflow.nodes[workflow.rootNodeId].nextNodeIds[0];

const timerCreated = await loops.createWorkflowNode(workflowId, {
  expectedRevisionId: workflowRevisionId,
  insertMode: "before",
  nodeTypeName: "TimerAction",
  toNodeId: exitNodeId,
});

const timerNodeId = timerCreated.node.id;
workflowRevisionId = timerCreated.workflow.workflowRevisionId;

const timer = await loops.updateWorkflowNode(workflowId, timerNodeId, {
  expectedRevisionId: workflowRevisionId,
  payload: { amount: 2, unit: "d" },
});

workflowRevisionId = timer.workflowRevisionId;

const emailCreated = await loops.createWorkflowNode(workflowId, {
  expectedRevisionId: workflowRevisionId,
  insertMode: "before",
  nodeTypeName: "SendEmailAction",
  toNodeId: exitNodeId,
});

const emailMessageId = emailCreated.node.emailMessageId;
workflowRevisionId = emailCreated.workflow.workflowRevisionId;
```

```php PHP SDK theme={"dark"}
use Loops\LoopsClient;

$loops = new LoopsClient('<your-api-key>');

$exit_node_id = $workflow['nodes'][$workflow['rootNodeId']]['nextNodeIds'][0];

$timer_created = $loops->workflows->createNode(
  workflow_id: $workflow_id,
  expected_revision_id: $workflow_revision_id,
  insert_mode: 'before',
  node_type_name: 'TimerAction',
  to_node_id: $exit_node_id,
);

$timer_node_id = $timer_created['node']['id'];
$workflow_revision_id = $timer_created['workflow']['workflowRevisionId'];

$timer = $loops->workflows->updateNode(
  workflow_id: $workflow_id,
  node_id: $timer_node_id,
  expected_revision_id: $workflow_revision_id,
  payload: [
    'amount' => 2,
    'unit' => 'd',
  ],
);

$workflow_revision_id = $timer['workflowRevisionId'];

$email_created = $loops->workflows->createNode(
  workflow_id: $workflow_id,
  expected_revision_id: $workflow_revision_id,
  insert_mode: 'before',
  node_type_name: 'SendEmailAction',
  to_node_id: $exit_node_id,
);

$email_message_id = $email_created['node']['emailMessageId'];
$workflow_revision_id = $email_created['workflow']['workflowRevisionId'];
```

```ruby Ruby SDK theme={"dark"}
exit_node_id = workflow["nodes"][workflow["rootNodeId"]]["nextNodeIds"][0]

timer_created = LoopsSdk::Workflows.create_node(
  workflow_id: workflow_id,
  expected_revision_id: workflow_revision_id,
  insert_mode: "before",
  node_type_name: "TimerAction",
  to_node_id: exit_node_id,
)

timer_node_id = timer_created["node"]["id"]
workflow_revision_id = timer_created["workflow"]["workflowRevisionId"]

timer = LoopsSdk::Workflows.update_node(
  workflow_id: workflow_id,
  node_id: timer_node_id,
  expected_revision_id: workflow_revision_id,
  payload: { amount: 2, unit: "d" },
)

workflow_revision_id = timer["workflowRevisionId"]

email_created = LoopsSdk::Workflows.create_node(
  workflow_id: workflow_id,
  expected_revision_id: workflow_revision_id,
  insert_mode: "before",
  node_type_name: "SendEmailAction",
  to_node_id: exit_node_id,
)

email_message_id = email_created["node"]["emailMessageId"]
workflow_revision_id = email_created["workflow"]["workflowRevisionId"]
```

```python Python theme={"dark"}
exit_node_id = workflow["nodes"][workflow["rootNodeId"]]["nextNodeIds"][0]

timer_created = requests.post(
    f"https://app.loops.so/api/v1/workflows/{workflow_id}/nodes",
    headers={
        "Authorization": "Bearer <your-api-key>",
        "Content-Type": "application/json",
    },
    json={
        "expectedRevisionId": workflow_revision_id,
        "insertMode": "before",
        "nodeTypeName": "TimerAction",
        "toNodeId": exit_node_id,
    },
).json()

timer_node_id = timer_created["node"]["id"]
workflow_revision_id = timer_created["workflow"]["workflowRevisionId"]

timer = requests.post(
    f"https://app.loops.so/api/v1/workflows/{workflow_id}/nodes/{timer_node_id}",
    headers={
        "Authorization": "Bearer <your-api-key>",
        "Content-Type": "application/json",
    },
    json={
        "expectedRevisionId": workflow_revision_id,
        "payload": {"amount": 2, "unit": "d"},
    },
).json()

workflow_revision_id = timer["workflowRevisionId"]

email_created = requests.post(
    f"https://app.loops.so/api/v1/workflows/{workflow_id}/nodes",
    headers={
        "Authorization": "Bearer <your-api-key>",
        "Content-Type": "application/json",
    },
    json={
        "expectedRevisionId": workflow_revision_id,
        "insertMode": "before",
        "nodeTypeName": "SendEmailAction",
        "toNodeId": exit_node_id,
    },
).json()

email_message_id = email_created["node"]["emailMessageId"]
workflow_revision_id = email_created["workflow"]["workflowRevisionId"]
```

## Delete a node and downstream nodes

Preview the impact with `dryRun: true`, then delete the node and its subtree. If contacts are queued on any node that would be removed, the API returns `"status": "queuedContactsFound"` — retry with `queuedContactPolicy: "discard"` to confirm.

Delete nodes [API reference](https://loops.so/docs/api-reference/delete-workflow-nodes.md) [CLI reference](https://loops.so/docs/cli/workflows.md#nodes-delete)

```bash CLI theme={"dark"}
loops workflows nodes delete $workflowId $nodeId \
  --recursive \
  --dry-run \
  --expected-revision-id $workflowRevisionId \
  -o json

loops workflows nodes delete $workflowId $nodeId \
  --recursive \
  --queued-contact-policy discard \
  --expected-revision-id $workflowRevisionId \
  -o json
```

```js JavaScript theme={"dark"}
const preview = await fetch(
  `https://app.loops.so/api/v1/workflows/${workflowId}/nodes/${nodeId}/recursive`,
  {
    method: "DELETE",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      expectedRevisionId: workflowRevisionId,
      dryRun: true,
    }),
  },
).then((response) => response.json());

console.log(preview.status, preview.nodeIds, preview.queuedContactCount);

const deleted = await fetch(
  `https://app.loops.so/api/v1/workflows/${workflowId}/nodes/${nodeId}/recursive`,
  {
    method: "DELETE",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      expectedRevisionId: workflowRevisionId,
      queuedContactPolicy: "discard",
    }),
  },
).then((response) => response.json());

workflowRevisionId = deleted.workflowRevisionId;
```

```js JavaScript SDK theme={"dark"}
import { LoopsClient } from "loops";

const loops = new LoopsClient("<your-api-key>");

const preview = await loops.deleteWorkflowNodesRecursive(workflowId, nodeId, {
  expectedRevisionId: workflowRevisionId,
  dryRun: true,
});

console.log(preview.status, preview.nodeIds, preview.queuedContactCount);

const deleted = await loops.deleteWorkflowNodesRecursive(workflowId, nodeId, {
  expectedRevisionId: workflowRevisionId,
  queuedContactPolicy: "discard",
});

workflowRevisionId = deleted.workflowRevisionId;
```

```php PHP SDK theme={"dark"}
use Loops\LoopsClient;

$loops = new LoopsClient('<your-api-key>');

$preview = $loops->workflows->deleteNodeRecursive(
  workflow_id: $workflow_id,
  node_id: $node_id,
  expected_revision_id: $workflow_revision_id,
  dry_run: true,
);

echo $preview['status'], implode(',', $preview['nodeIds']), $preview['queuedContactCount'];

$deleted = $loops->workflows->deleteNodeRecursive(
  workflow_id: $workflow_id,
  node_id: $node_id,
  expected_revision_id: $workflow_revision_id,
  queued_contact_policy: 'discard',
);

$workflow_revision_id = $deleted['workflowRevisionId'];
```

```ruby Ruby SDK theme={"dark"}
preview = LoopsSdk::Workflows.delete_node_recursive(
  workflow_id: workflow_id,
  node_id: node_id,
  expected_revision_id: workflow_revision_id,
  dry_run: true,
)

puts preview["status"], preview["nodeIds"], preview["queuedContactCount"]

deleted = LoopsSdk::Workflows.delete_node_recursive(
  workflow_id: workflow_id,
  node_id: node_id,
  expected_revision_id: workflow_revision_id,
  queued_contact_policy: "discard",
)

workflow_revision_id = deleted["workflowRevisionId"]
```

```python Python theme={"dark"}
preview = requests.delete(
    f"https://app.loops.so/api/v1/workflows/{workflow_id}/nodes/{node_id}/recursive",
    headers={
        "Authorization": "Bearer <your-api-key>",
        "Content-Type": "application/json",
    },
    json={
        "expectedRevisionId": workflow_revision_id,
        "dryRun": True,
    },
).json()

print(preview["status"], preview["nodeIds"], preview["queuedContactCount"])

deleted = requests.delete(
    f"https://app.loops.so/api/v1/workflows/{workflow_id}/nodes/{node_id}/recursive",
    headers={
        "Authorization": "Bearer <your-api-key>",
        "Content-Type": "application/json",
    },
    json={
        "expectedRevisionId": workflow_revision_id,
        "queuedContactPolicy": "discard",
    },
).json()

workflow_revision_id = deleted["workflowRevisionId"]
```

## Edit a workflow email's content

Each `SendEmailAction` node includes an `emailMessageId`. Pass that ID to the
email message content APIs to read or edit the email's subject, LMX body,
sender, and more.

First fetch the current content to get its `contentRevisionId`, then send an
update.

> [!TIP]
>
> Save the returned `contentRevisionId` after each update and pass it as
> `expectedRevisionId` on the next update to avoid `409 Conflict` errors caused
> by stale revisions.

Get email message [API reference](https://loops.so/docs/api-reference/get-email-message.md) [CLI reference](https://loops.so/docs/cli/email-messages.md#get)\
Update email message [API reference](https://loops.so/docs/api-reference/update-email-message.md) [CLI reference](https://loops.so/docs/cli/email-messages.md#update)

```bash CLI theme={"dark"}
loops email-messages get $emailMessageId -o json

loops email-messages update $emailMessageId \
  --expected-revision-id $contentRevisionId \
  --subject "Welcome to Loops 👋" \
  --lmx '<Paragraph><Text>Thanks for signing up!</Text></Paragraph>'
```

```js JavaScript theme={"dark"}
// emailMessageId comes from a SendEmailAction node (see above).
const emailMessageId = node.emailMessageId;

// Fetch the current content to get the latest contentRevisionId.
const current = await fetch(
  `https://app.loops.so/api/v1/email-messages/${emailMessageId}`,
  {
    method: "GET",
    headers: {
      "Authorization": "Bearer <your-api-key>",
    },
  },
).then((response) => response.json());

const response = await fetch(
  `https://app.loops.so/api/v1/email-messages/${emailMessageId}`,
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      expectedRevisionId: current.contentRevisionId,
      subject: "Welcome to Loops 👋",
      lmx: "<Paragraph>Thanks for signing up!</Paragraph>",
    }),
  },
);

const updated = await response.json();
const nextContentRevisionId = updated.contentRevisionId;
```

```js JavaScript SDK theme={"dark"}
import { LoopsClient } from "loops";

const loops = new LoopsClient("<your-api-key>");

const emailMessageId = node.emailMessageId;

const current = await loops.getEmailMessage(emailMessageId);

const updated = await loops.updateEmailMessage(emailMessageId, {
  expectedRevisionId: current.contentRevisionId,
  subject: "Welcome to Loops 👋",
  lmx: "<Paragraph><Text>Thanks for signing up!</Text></Paragraph>",
});

const nextContentRevisionId = updated.contentRevisionId;
```

```php PHP SDK theme={"dark"}
use Loops\LoopsClient;

$loops = new LoopsClient('<your-api-key>');

$email_message_id = $node['emailMessageId'];

$current = $loops->emailMessages->get(email_message_id: $email_message_id);

$updated = $loops->emailMessages->update(
  email_message_id: $email_message_id,
  expected_revision_id: $current['contentRevisionId'],
  subject: 'Welcome to Loops 👋',
  lmx: '<Paragraph><Text>Thanks for signing up!</Text></Paragraph>',
);

$next_content_revision_id = $updated['contentRevisionId'];
```

```ruby Ruby SDK theme={"dark"}
email_message_id = node["emailMessageId"]

current = LoopsSdk::EmailMessages.get(email_message_id: email_message_id)

updated = LoopsSdk::EmailMessages.update(
  email_message_id: email_message_id,
  expected_revision_id: current["contentRevisionId"],
  subject: "Welcome to Loops 👋",
  lmx: "<Paragraph><Text>Thanks for signing up!</Text></Paragraph>",
)

next_content_revision_id = updated["contentRevisionId"]
```

```python Python theme={"dark"}
# email_message_id comes from a SendEmailAction node (see above).
email_message_id = node["emailMessageId"]

# Fetch the current content to get the latest contentRevisionId.
current = requests.get(
    f"https://app.loops.so/api/v1/email-messages/{email_message_id}",
    headers={
        "Authorization": "Bearer <your-api-key>",
    },
).json()

response = requests.post(
    f"https://app.loops.so/api/v1/email-messages/{email_message_id}",
    headers={
        "Authorization": "Bearer <your-api-key>",
        "Content-Type": "application/json",
    },
    json={
        "expectedRevisionId": current["contentRevisionId"],
        "subject": "Welcome to Loops 👋",
        "lmx": "<Paragraph>Thanks for signing up!</Paragraph>",
    },
)

updated = response.json()
next_content_revision_id = updated["contentRevisionId"]
```


## Related topics

- [API Examples](https://loops.so/docs/api-reference/examples.md)
- [Events API examples](https://loops.so/docs/api-reference/examples/events.md)
- [Contacts API examples](https://loops.so/docs/api-reference/examples/contacts.md)
- [Campaigns API examples](https://loops.so/docs/api-reference/examples/campaigns.md)
- [Transactional email API examples](https://loops.so/docs/api-reference/examples/transactional-emails.md)
