Durable control flow API
intheloop.run
Human-in-the-loop control flow, built on Resonate.
A small API that exposes a single abstraction: a loop that blocks on human decisions and resumes execution when the promises resolve.
LOOP MODEL
A loop awaits N promises before execution continues.
Humans accept or reject promises explicitly. The loop resolves with an ACCEPTED or REJECTED result once every participant is complete.
Step 1
Create the loop
POST /v1/loop/create with loop_name, loop_description, participants, and participant_timeout. The timeout applies per participant promise.
POST /v1/loop/create
Step 2
Distribute accept/reject links
Each participant receives accept_link and reject_link that resolve their promise.
GET /v1/loop/participant/resolve/:promiseId
Step 3
Resolve explicitly
Resolve a promise with accept=true|false.
POST /v1/loop/participant/resolve/:promiseId
Step 4
Check state
Poll status while promises are pending. Timeouts resolve the loop as REJECTED.
GET /v1/loop/status/:loopId
Step 5
Fetch the result
Get the workflow result, including per-participant decisions.
GET /v1/loop/result/:loopId
API SURFACE
Loop API endpoints
The loop lifecycle uses five endpoints; the root path returns metadata. Create is authenticated; resolve and result are bound to loop and promise IDs.
Service metadata
Returns environment details and the current endpoint map.
Create a loop
Start a loop with context, participant emails, and a timeout. Returns accept/reject links.
Request: loop_name, loop_description, participants[], participant_timeout
Response: success, message, data.loop_data{ loop_id, loop_name, loop_description, participant_timeout, participants[{ email, promise_id, accept_link, reject_link }] }
Requires API key.
Check loop status
Return promise state plus a decision status if the loop resolved.
Response: success, message, data{ loop_id, state, resolved, status, message, result }
Status is PENDING until the loop resolves.
Resolve a promise
Resolve one participant promise with accept=true|false.
Request: accept
Response: success, message, data{ promise_id, state }
Resolve via link
Resolve a promise via a shareable link.
Required query string: ?accept=true|false
Fetch loop result
Return the resolved loop result data.
Response: success, message, data{ loop_id, state, result }
Returns 409 while pending.
AUTH & ACCESS
Explicit boundaries, minimal auth surface.
API keys gate loop creation. Resolve links and loop IDs are the boundaries for follow-up calls, so share them intentionally.
x-api-key: <api_key>
Authorization: Bearer <api_key>
REQUEST BODIES
Request bodies are small and explicit.
Create requires four fields. Resolve requires an accept boolean.
Create loop body
{
"loop_name": "agent output check",
"loop_description": "review the agent output for accuracy",
"participants": ["reviewer@example.com"],
"participant_timeout": 900000
}Resolve body
{
"accept": false
}HOMEGROWN AGENT
A homegrown agent using only the HTTP API.
The agent produces a draft response, asks a human to accept or reject, and retries with updated information until the loop resolves as ACCEPTED.
The core mechanic is simple: create a loop, wait for status, and repeat if rejected. The helper functions are thin wrappers around the create and status endpoints.
Homegrown agent example
const apiBaseUrl = "http://localhost:7000";
const apiKey = process.env.API_KEY;
const reviewer = "reviewer@example.com";
async function runAgentReview(subject, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const draft = buildDraft(subject, attempt);
const loop = await createLoop(apiBaseUrl, apiKey, {
loop_name: `agent review #${attempt}`,
loop_description: draft,
participants: [reviewer],
participant_timeout: 10 * 60_000,
});
const acceptLink = loop.loop_data.participants[0].accept_link;
console.log("awaiting decision:", acceptLink);
const status = await waitForResolution(apiBaseUrl, apiKey, loop.loop_data.loop_id);
if (status.status === "ACCEPTED") {
return status.result;
}
console.log("not accepted, retrying:", status.message);
}
throw new Error("no accepted result");
}HOW IT WORKS
The loop is a Resonate durable function.
intheloop.run is implemented as a Resonate workflow. Each participant promise is created inside the workflow, and the generator suspends at each yield* until a promise resolves.
When a participant clicks accept or reject, Resonate replays the workflow from the last checkpoint and continues execution deterministically.
Loop workflow (Resonate)
import { Resonate, type Context } from "@resonatehq/sdk";
function* loopWorkflow(ctx: Context, input: LoopInput) {
const promises = input.promise_ids.map((id, index) =>
ctx.promise({
id,
timeout: input.participant_timeout_ms,
tags: {
loop_id: input.loop_id,
participant_email: input.participant_emails[index],
},
})
);
const results = [];
for (const promiseHandle of promises) {
const decision = yield* promiseHandle; // durable checkpoint
results.push(decision?.data ?? {});
}
const rejected = results.find((r) => r?.accept === false);
return rejected
? { status: "REJECTED", message: "participant rejected", results }
: { status: "ACCEPTED", message: "all participants accepted", results };
}USAGE NOTICE
This is a demonstration service.
By using this service you agree not to abuse, harass, or harm others, and to use it only for lawful and respectful purposes. We may monitor usage for operational reasons and reserve the right to suspend or delete accounts at any time. We may contact account owners to understand their use cases and interest in Resonate.
intheloop.run is an example project brought to you by Resonate HQ. It is not a managed production product and is not monitored as such.