List workflows
Retrieve a paginated list of workflows, most recently updated first. API referenceCLI 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.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 anodeId, then fetch the full node
configuration.
API referenceCLI 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, follownextNodeIds 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)
Create a workflow with an event trigger
Create a draft workflow (blank trigger + exit), look up an event pattern, then configure the root trigger as anEventTrigger.
Create workflow API reference CLI referenceUpdate node API reference CLI reference
Get event pattern by name API reference CLI reference
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
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;
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;
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'];
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"]
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 (SignupTrigger) root node to 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 CLI referenceChange mailing list API reference CLI reference
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
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;
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;
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'];
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"]
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 withinsertMode: "before" (or "between"). New nodes start with defaults — configure the timer after creation. Email content is edited separately with the email message APIs.
Create node API reference CLI referenceUpdate node API reference CLI reference
# 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
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;
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;
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'];
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"]
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 withdryRun: 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 CLI reference
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
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;
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;
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'];
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"]
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
EachSendEmailAction 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.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"]

