Skip to main content
Workflow API endpoints are currently in alpha and are subject to change.

List workflows

Retrieve a paginated list of workflows, most recently updated first. API reference
CLI reference
loops workflows list -o json
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;
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;
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'];
response = LoopsSdk::Workflows.list(perPage: 20)

workflow_id = response["data"][0]&.dig("id")
next_cursor = response["pagination"]["nextCursor"]
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.
Save rootNodeId and the keys in nodes. Use them to request full node details with Get workflow node.
API reference
CLI reference
loops workflows get $workflowId -o json
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 ?? [];
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 ?? [];
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'] ?? [];
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") || []
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
CLI reference
loops workflows nodes get $workflowId $nodeId -o json
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);
}
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);
}
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'];
}
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
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.
for nodeId in $nextNodeIds; do
  loops workflows nodes get $workflowId $nodeId -o json
done
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);
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);
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);
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)
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)

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.
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 CLI reference
Update email message API reference CLI reference
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>'
// 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;
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;
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'];
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"]
# 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"]
Last modified on July 2, 2026