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 IDs are root promises; results include ACCEPTED or REJECTED.
participant_timeout is applied per participant promise.
Create requires an API key. Other calls use loop or promise IDs.

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.

GET /

Service metadata

Returns environment details and the current endpoint map.

POST /v1/loop/create

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.

GET /v1/loop/status/:loopId

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.

POST /v1/loop/participant/resolve/:promiseId

Resolve a promise

Resolve one participant promise with accept=true|false.

Request: accept

Response: success, message, data{ promise_id, state }

GET /v1/loop/participant/resolve/:promiseId

Resolve via link

Resolve a promise via a shareable link.

Required query string: ?accept=true|false

GET /v1/loop/result/:loopId

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.