Skip to main content

Run columns through the API and read the results

This article shows you how to add columns from templates, price a run, test one row, run the rest, read the results, stop a run and set up a schedule through the Loopster API or the MCP tools.

Written by Umer Ishaq

Before you start

  • Key: API calls need a Loopster API key. See Create and revoke a Loopster API key. Send it as Authorization: Bearer loopster_sk_…. An MCP client can send the same header, or sign in through OAuth.

  • Address: the base URL is https://app.loopster.ai/api. Every path starts with /v1.

  • Workspace: your key decides which workspace you work in. No call takes a workspace id.

  • Ids: table, row and run ids are UUIDs. Column ids start with col_. A table id in the path that is not a UUID gets a 400, not a 404.

  • Auto-run: the API calls a table's Auto-run setting autoplay. Read it with GET /v1/tables/{tableId}. Change it with PATCH /v1/tables/{tableId} (MCP: set_table_autoplay). Check it on every table before you insert rows.

  • Important: with Auto-run on, rows you insert are queued and can spend from your wallet. With it off, an insert only writes rows. Switching it on does not run the rows already in the table.

  • MCP: the MCP tool for each step is in parentheses. Where a tool takes a column, you can give its name or its id.

Which calls can spend from my wallet?

These calls can start paid work, so they can spend from your wallet:

  • Triggering rows with POST /v1/tables/{tableId}/trigger, unless it is a dry run (MCP: trigger_rows).

  • Running a function with POST /v1/functions/{functionId}/runs (MCP: run_function). Every step inside the function bills as it would in a table.

  • Running a capability with POST /v1/capabilities/{templateId}/runs (MCP: run_capability). It spends from your wallet unless it runs on your own connection.

  • Creating a table from a paid starter with POST /v1/tables (MCP: create_table). A blank, webhook or search table is free.

  • Inserting rows with POST /v1/tables/{tableId}/rows, or a row arriving through the table's webhook, when the table has Auto-run on (MCP: insert_rows).

  • Creating a schedule with POST /v1/tables/{tableId}/schedules (MCP: create_schedule). It spends each time it fires, until it is paused or deleted.

  • Resuming a schedule with PATCH /v1/tables/{tableId}/schedules/{scheduleId} and paused: false (MCP: update_schedule). It spends when it fires.

  • Updating a row or cells with PATCH /v1/tables/{tableId}/rows/{rowId} or PATCH /v1/tables/{tableId}/cells (MCP: update_row, update_cells). The tool descriptions say that on a table with auto-rerun columns, writing a cell also re-queues the columns that read it, and that spends from your wallet.

These do not start paid work by themselves: a dry run (estimate_trigger_cost), creating a blank or webhook table, inserting rows into a table with Auto-run off, and reading a trace. Stop and cancel prevent more spending. They do not give money back.

For prices in the app and where to add funds, see Your wallet and keeping spend under control.

How do I add a column from a template?

  1. List the templates with GET /v1/tables/{tableId}/columns/templates (list_column_templates). Narrow the list with type (manual, ai, api or task), provider or tab (import, enrich, tools, functions or export).

  2. Read the one you want with GET /v1/tables/{tableId}/columns/templates/{templateId} (get_column_template). It shows the template's settings and defaults. For a task template it also gives the JSON Schema of its taskConfig.

  3. Check the template's cost. The next section explains it.

  4. Create the column with POST /v1/tables/{tableId}/columns and a templateId (create_column). To add up to 20 at once, send {"columns": [ ... ]} (create_columns). They are created in order, and it is all or nothing. If one is refused, none is stored.

  5. Set the column up before you run it with PATCH /v1/tables/{tableId}/columns/{columnId} (update_column). A column left on a template's defaults runs on those defaults and bills for them. An AI column made from a model template comes with a model and no prompt. It does not run until you set aiConfig.prompt.

What does a template's cost tell me?

Each template can carry a cost that says what one row costs from your wallet. Its kind is one of:

  • free: Loopster charges nothing.

  • byok: the column runs on your own connected account, so nothing leaves the wallet.

  • usage_based: there is no single figure. The steps inside bill as they run.

  • per_row: a dollar amount per row, in usd.

For AI templates and waterfalls the per-row figure is approximate. A waterfall also has maxUsd, the most one row can cost. A manual or api template has no cost key. A missing cost means a price does not apply. It does not mean the price is zero.

How do I see what a run will cost first?

  1. Send the request you plan to make with ?dryRun=true on the address: POST /v1/tables/{tableId}/trigger?dryRun=true (MCP: estimate_trigger_cost, which takes the same arguments as trigger_rows). Nothing is queued and nothing is charged.

  2. Read the size of the run in rows, columns and cellsToRun. perColumn breaks it down by column.

  3. Read maxCostUsd. It is a ceiling, not a forecast. It leaves out any column whose price cannot be known before it runs. unpricedColumns says how many.

  4. Read wallet. spendableUsd is what the wallet can spend now. rowsCovered is how many rows it covers. wouldStart says whether the run would start at all.

Two things to know about the estimate:

  • Run conditions are not evaluated. Fewer rows than quoted may run, never more.

  • A cell that already holds a finished answer is left out, because an unforced trigger does not re-run it. To price a forced re-run, send force: true in the dry run too.

How do I test one row, then run the rest?

Important: without rowIds, a trigger runs every row in the table and spends accordingly. Always send rowIds, and price the run with a dry run first. Through MCP, call estimate_trigger_cost first, or pass specific rowIds.

  1. Pick one row. Its id comes from list rows, GET /v1/tables/{tableId}/rows.

  2. Price it. Send POST /v1/tables/{tableId}/trigger?dryRun=true with the body {"rowIds": ["{rowId}"], "columnIds": ["{columnId}"]}. columnIds limits the run to the columns you name.

  3. Run it. Send the same request without ?dryRun=true. The answer comes back at once with a runId, and the work continues in the background.

  4. Check the result. See the next sections.

  5. Price the rest, then run it. Send the other row ids as rowIds, in batches of 10,000 or fewer. An unforced trigger does not re-run a cell that already holds a finished answer, and does not charge for it. force: true re-processes completed cells.

This is the same habit as in the app. See Run columns safely: Auto-run, test one row, then run the rest.

How do I cap what a trigger can cost?

Send maxCostUsd. Loopster estimates the run first, the way a dry run does. If the estimate is over the cap, or the run includes a column with no up-front price and you did not send allowUnpriced: true, nothing is queued and the answer is 422. The cap is a check at the start, not a meter. A run that starts is not stopped part-way, and an AI column's real cost can come in above its estimate.

How do I retry a trigger without running it twice?

Send an Idempotency-Key header (MCP: idempotencyKey) so a retry is safe. Use 1 to 128 characters: letters, digits, _, -, . or :. Use a new key for each new action. Repeat the same request with the same key within 24 hours and you get the first answer back, with "replayed": true. Nothing runs or bills twice.

How do I check how a run is going?

Enrichment is asynchronous and can take minutes, so poll.

  • GET /v1/runs/{runId} (get_run) reports on one run. status is running until every cell of the run has settled, then complete or completed_with_errors. It is stopped if a stop reached any of its cells. cells counts the run's cells: total, inFlight, complete, error, skipped, unsatisfied and notRun. notRun means never reached, or reset by a stop.

  • GET /v1/runs/{runId}/items (list_run_items) lists the rows in a run. Each row shows its current state: running, complete, error, incomplete or not_run. A row is complete when all its cells settled without an error. A skipped or unsatisfied cell counts as settled. incomplete means some cells settled and the rest never started, which a stop leaves behind. A trigger run returns row ids and states, never cell values.

  • GET /v1/tables/{tableId}/status (get_status) reports on the whole table. rows counts total, idle, queued, generating, complete and error. columns lists every column with its own counts, whether or not it has ever run. It covers all runs on the table, not only yours.

  • GET /v1/runs (list_runs) lists the workspace's runs, newest first. Filter with kind, status and since. limit is 1 to 100 (50 by default), and cursor gets the next page.

For the items call, limit is 1 to 100 (100 by default). Pass the previous nextCursor as cursor. A cursor works only for the run it came from.

A few things to know:

  • A run is never reported finished in its first 30 seconds. A faster run still reads running until then.

  • The answer for one run is cached for up to 5 seconds. If it is still being worked out, cells is null with a Retry-After header, or the last answer comes back with stale: true.

  • The run list shows what is stored, so a finished run nobody has read since can still read running there. Use the single run for live counts.

  • A workspace keeps its 500 most recent runs, for up to 30 days. After that the runId answers 404.

How do I read the results?

  1. Call list rows: GET /v1/tables/{tableId}/rows (list_rows). page is 1 by default. pageSize is 50 by default, up to 500. columns limits which cells come back.

  2. Read each row. cells holds the values, keyed by column id. cellStatuses tells an empty value apart from a skipped, failed or never-run one. cellErrors holds the message for a failed cell.

  3. Read the row's status. It is worked out from its cells: still working if any cell is, otherwise error if any cell failed, otherwise complete once anything has run, otherwise idle.

  4. To read one row in full, call GET /v1/tables/{tableId}/rows/{rowId} (get_row). Each cell comes with its value, its status and the error message if it failed.

To find which rows of a run failed, list the run's items, then read those rows. In the app, a failed cell shows its message in the cell, and the labels are Error, Unsatisfied and Skipped. See What a cell's message means.

Why is a waterfall cell empty, and which provider answered?

Call GET /v1/tables/{tableId}/rows/{rowId}/trace (get_waterfall_trace). Pass columnId to trace one waterfall column, or leave it out to trace every traceable column on the row. It only reads. It never re-runs anything.

The answer shows:

  • What the waterfall settled on: result, status and reason, and whether the answer was verified.

  • Every provider it tried, in order: order, provider, status, outcome, reason and won. The outcome is one of found, no_match, not_verified, duplicate, out_of_date, wrong_line_type, not_used, skipped or error.

  • Two costs: chargedUsd is what the wallet was billed for the run. attemptedUsd is what the individual steps consumed, plus any check run on a shared-pool address before a step. Attempted can be higher than charged where only the successful lookup is billed.

If successfulProvider is shared_pool, the answer came from Loopster's shared contact pool. No step ran, so attempts is empty. That is free, unless the column verifies its answers and the pooled address had to be checked first. Then the cost is that one check.

How do I stop a run, and is cancel a refund?

You have two brakes. Both prevent further spending. Neither is a refund.

How do I stop everything on a table?

Send POST /v1/tables/{tableId}/stop (MCP: stop_run, which takes a tableId). It stops all ongoing processing for the table. Work that was already charged is not refunded.

Two paragraphs from the Stop Processing page of the Loopster API docs. The first says to stop all active processing for a table and that it requires a Bearer token. The second says the response reports how much was released, and that a null count is not the same as zero.

The answer reports:

  • queuedJobsRemoved: queued work removed before it could start. Nothing was charged for it.

  • cellsReleased and rowsReleased: what the stop released.

  • stillRunning: the cells that work already past the brake may still run, and bill for.

  • cleanupContinues: when it is true, the released counts are lower bounds so far. Nothing new starts either way.

A null count means the worker did not report it. It is not the same as zero. You can call stop twice. With nothing running it reports zero.

How do I cancel one run?

Send POST /v1/runs/{runId}/cancel (MCP: cancel_run). It removes the work of one run that is still queued, across the columns it ran. Work already running finishes and is billed.

The warning box on the Cancel a Run page of the Loopster API docs: A cancel is not a refund. Work that was already running when the cancel arrived finishes and is billed. The answer says how much was stopped and how much was already running.

The answer gives stoppedCells (queued cells removed before they started, with nothing charged) and alreadyRunningCells (they finish and are billed). A row that another run had already queued is not part of this run, so only that run's cancel stops it. Cancelling a run that already finished, or cancelling twice, changes nothing. The answer says canceled: false with the stored status. Any editor in the run's workspace may cancel any run there.

What happens if my wallet runs short?

Paid work holds its worst-case cost from your wallet before it runs, and is charged what it really cost, never more. The wallet is never overdrawn.

  • If a run is bigger than the wallet, rows run in order while the wallet covers them. The rest are refused, not run. Their cells show a not-enough-balance error, and nothing is charged for them.

  • If the wallet cannot cover even the first row, the call is refused with a 402 before anything is queued.

Three bullets from the Trigger Processing page of the Loopster API docs about what happens when the wallet runs short. A run bigger than the wallet runs what fits. A run that cannot cover even its first row is refused up front with a 402 insufficient_balance. A dry run shows how many rows the wallet covers and whether the run would start.

When a call that would start paid work is refused with a 402, nothing was created, queued or charged. The body carries code, requiredUsd (the most the first row or input can cost) and spendableUsd (what the wallet can spend right now, capped by the workspace's spend limit if one is set). The code says which limit refused the call:

  • insufficient_balance: the wallet cannot cover it. Click the wallet button at the top right, then Manage wallet. Settings opens on Plan & billing, on the Wallet tab, with a Top up button and an Auto top-up card.

  • workspace_limit: the workspace's own spend limit leaves less than the wallet. Open Settings, then Workspaces, click the workspace and change Spend limit with Set spend limit.

  • daily_cogs_limit: the account reached its daily spend safety limit. Topping up does not lift it. It resets at midnight UTC, and Loopster support can raise it.

If the refused call carried an Idempotency-Key, the key is released. The same key runs once you have topped up.

To look before you run, read wallet in a dry run. GET /v1/wallet (get_wallet) shows how much the next run can spend, the workspace's plan and any spend limit on the workspace. GET /v1/usage (get_usage) shows what the workspace spent, by table, by column or by day. Amounts are US dollars. The wallet holds real dollars, not credits. See Top up your wallet, set auto top-up and read your funds history and Workspaces: create one and set a spend limit.

How do I run columns on a schedule?

A schedule runs columns on a table again and again, on its own. Schedules work over both the API and MCP.

Important: a schedule spends from your wallet every time it fires, with nobody watching, until you pause or delete it. Send paused: true to save one without letting it fire.

  1. Create it with POST /v1/tables/{tableId}/schedules (create_schedule). This needs editor access. Listing schedules with GET /v1/tables/{tableId}/schedules (list_schedules) needs member access.

  2. Name the columns: 1 to 50, by id or by name when no other column shares it. Each must be an AI, API or task column that is set up to run.

  3. Set the timing. cadence is daily, weekly, monthly or interval. time is HH:MM on a 24-hour clock (for interval, the time of the first run). dayOfWeek runs from 0 (Sunday) to 6 (Saturday). dayOfMonth runs from 1 to 28. every is {count, unit} in hours, days or weeks, at least one hour.

  4. Choose which cells each run covers with cells: empty, error, unsatisfied or all. The default is all. force defaults to true and rerunDependents to false. Check these before the first firing.

  5. Add timezone, an IANA name such as Europe/Berlin. It defaults to UTC. Loopster converts the time to UTC at the zone's current offset, so a daylight-saving change moves the run by an hour. Times are stored and returned in UTC.

To change a schedule, send PATCH /v1/tables/{tableId}/schedules/{scheduleId} (update_schedule). Only the fields you send change, except the timing. The timing is replaced whole: if you send any of cadence, time, timezone, dayOfWeek, dayOfMonth or every, send cadence and time with it.

  • paused: true takes the schedule off the scheduler so it cannot fire. paused: false starts it again, and it spends when it fires.

  • DELETE /v1/tables/{tableId}/schedules/{scheduleId} (delete_schedule) makes it never fire again. A run it already started is not stopped. POST /v1/tables/{tableId}/stop does that.

  • A table can have 10 active schedules. Resuming a paused one counts against that limit.

  • A schedule records which key made it. If that key is revoked, or expires before a firing, Loopster pauses the schedule and pausedReason reads key_revoked or key_expired. For a schedule you paused yourself, pausedReason is null. Resuming clears it.

  • Each schedule in the list also shows lastRunAt, nextRunAt and a target. A flow target runs the table's whole flow, as set up in the app.

Can I see an API schedule in the app?

Yes. A schedule made over the API is stored the way the app's schedule editor stores it, so you can open and change it there. Click Table settings next to the table name. The panel has a Scheduled column reruns section. In the toolbar, Scheduled column runs shows what is scheduled. See Table settings: Auto-run, scheduled reruns and deduplication.

Troubleshooting

Fewer rows started than I sent

What you see: The trigger answered 200, but triggeredCount is lower than the number of rows you sent.

Why it happens: triggeredCount is the rows that were actually enqueued. deduplicated rows already had a run in progress, and nothing extra is charged for them. rejected rows were refused because the queue is full. They did not start and will not start on their own. unavailable shows only when it is above zero. Those rows could not be confirmed, so they may or may not have started. The four numbers add up to what you asked for, and a message says why when some rows did not start.

What to do: Trigger the rejected rows again. For unavailable rows, retrying an unforced row is safe, because a row that did start counts as deduplicated. Retrying a forced run (force: true) can start a row twice.

My trigger answered 503 or 500

What you see: A 503 with code: nothing_started and a Retry-After header, or a 500.

Why it happens: Loopster could not hand the run to its workers, and the request to them was never sent. Nothing was queued and nothing was charged. A 500 with code: nothing_started_internal also started nothing, but it is a fault on Loopster's side, so sending it again now will likely fail the same way. Any other 500 makes no promise: rows may have started.

What to do: After a 503, send the same request again after Retry-After seconds. Use the same Idempotency-Key if you sent one, because this answer is not stored against it. After nothing_started_internal, try later. After any other 500, rows may have started, so check the table's status before you send again.

My trigger answered 400

What you see: A 400, and nothing runs.

Why it happens: A request covers at most 10,000 rows. With no rowIds, a table of more than 10,000 rows is refused. A 400 also comes back when a rowIds entry is not a UUID, when a columnIds entry is not a column of this table, or when none of your row ids belong to this table. If only some belong to another table, those are skipped and listed in notInTable.

What to do: Send rowIds in batches of 10,000 or fewer. Take ids from list rows and list columns. An empty rowIds list runs nothing.

My trigger answered 422

What you see: A 422 with code: cost_cap_exceeded or cost_cap_unpriced_columns.

Why it happens: You sent maxCostUsd, and nothing was queued. cost_cap_exceeded means the estimate is over the cap. cost_cap_unpriced_columns means a column has no up-front price and you did not send allowUnpriced.

What to do: Raise the cap, run fewer rows, or send allowUnpriced: true with the cap. allowUnpriced without maxCostUsd is a 400. This refusal releases an Idempotency-Key, so the same key works once the cap is raised.

I topped up but still get a 402

What you see: A 402 with code: daily_cogs_limit after you added funds, and spendableUsd is 0.

Why it happens: daily_cogs_limit is the account's daily spend safety limit. It is not a low balance. Topping up does not lift it, and asking for less does not help.

What to do: Wait for it to reset at midnight UTC, or ask Loopster support to raise it. See Contact support.

The run still says running, or cells is null

What you see: A run you expect to be done reads running, or cells is null.

Why it happens: A run is never reported finished in its first 30 seconds. The run list shows what is stored, so it can lag. When another request is still working out the answer, cells is null with a Retry-After header.

What to do: Ask again after the Retry-After seconds. Use GET /v1/runs/{runId} for live counts. If the run items call answers 503 with Retry-After, wait and call it again.

The call succeeded but some cells show errors

What you see: The calls answered success and the run finished, but some cells failed.

Why it happens: A call can succeed while single cells fail to enrich. The status call counts how many cells errored, but not why.

What to do: List the run's items, find the rows in error, and read those rows. cellErrors in list rows, or get_row, gives the message. In the app, the failed cell says so, and the footer shows a count such as "1 cell failed".

I stopped a run and was still charged

What you see: The wallet went down after you called stop or cancel.

Why it happens: Stop and cancel prevent further spending. They do not refund. Work that was already charged stays charged, and work already running finishes and is billed.

What to do: Read stillRunning in the stop answer, or alreadyRunningCells in the cancel answer. A null stillRunning means unknown, not zero. Next time, price the run with a dry run and send maxCostUsd.

Good to know

  • Deleting a table does not stop work that is already running. Stop the run first, or the queued work finishes and bills against the table in Trash.

  • Through the API, a column you create without naming an account never starts on Loopster's account. The exceptions are a provider whose default your workspace saved as Loopster's account, and an action that runs only on Loopster's account. Name an account with integrationConnectionId on the column.

  • If an Idempotency-Key cannot be used, you get a 409 and nothing runs. idempotency_key_reused means the key was used for a different request. idempotency_key_in_progress means the first request is still running, so retry after Retry-After seconds. idempotency_outcome_unknown means the first request never finished, so whether it started work is unknown.

  • A stored failure replays as the same failure. Send a new key to try again.

  • A 429 means a rate limit was reached and nothing was done. Wait the seconds in the Retry-After header, then send the call again.

  • Six MCP read tools take response_format: list_tables, get_table, list_columns, list_rows, get_row and get_status. detailed is the default. concise keys cells and counts by column name and truncates long values. Use detailed before you change a column.

  • The full list of calls is the OpenAPI file at https://app.loopster.ai/openapi.json.

More about the API and MCP

Did this answer your question?