All docs
4 min read

Bulk actions

Sometimes one submission isn't enough. Sometimes you've got thirty rows of crypto spam in your inbox and you want them gone in one click.

Bulk actions are how.

Selecting rows

In the inbox or spam folder, the leftmost column is a checkbox. Click rows individually, or use the header checkbox to select all on this page. There's also a Select all matching filters option that appears once you've selected any row — extends the selection to every row matching your current filters, even ones not yet rendered.

Selected rows surface a bulk action bar at the top of the list. The bar tells you how many rows are selected and which actions are available.

The 500-row cap

A single bulk operation is capped at 500 submissions. Hard limit, enforced both in the UI and the API.

Why 500? Two reasons:

  1. Keeps each operation tight enough to run synchronously without long request times.
  2. Caps blast radius on a misclick. Accidentally deleting 500 rows is recoverable from your backups (we keep daily snapshots on Pro+); accidentally deleting 500,000 isn't a fun support ticket.

If you need to operate on more than 500, do it in batches. The selection UI counts down as you go, and "Select all matching filters" auto-pages to the next 500 after a successful run.

Available actions

delete

Permanently deletes the selected submissions. No trash, no undo. Files go too — we don't leave orphaned uploads.

The action bar shows a confirmation modal that requires you to type the count (Delete 47 submissions) — a deliberate friction point so a misclick doesn't nuke an inbox.

mark_spam

Flips status to spam and moves the selected rows to the spam folder. Use it when something slipped past the spam stack — e.g. a new pattern of unsolicited "SEO services" pitches.

If a row was already in the spam folder, this is a no-op for that row (the bulk count still includes it).

mark_not_spam

The reverse: flips status from spam back to received and moves rows to the inbox. Each recovered row fires its notifications, autoresponder, and webhooks as if it had landed clean originally — so don't bulk-recover unless you want that.

For high-volume recoveries (e.g. you wrote a too-aggressive custom rule and want to undo it), expect a brief flood of notification emails. Disable notifications on the form first if you're recovering hundreds of rows.

REST parity

The same actions are exposed over the API:

POST /api/v1/submissions/bulk
Content-Type: application/json
Authorization: Bearer ...

{
  "action": "mark_not_spam",
  "ids": ["01K2M...", "01K2N...", "01K2P..."]
}

Required: action (one of delete, mark_spam, mark_not_spam), ids (1–500 ULIDs).

Response: { "ok": true, "affected": 47 }. If any ID is invalid or scoped to a different team, we return 422 with the offending IDs — no partial commits, the whole batch rolls back.

MCP parity

bulk_submissions(
  action="delete",
  ids=["01K2M...", "01K2N...", ...]
)

Same shape, same 500-row cap, same all-or-nothing semantics. Requires the submissions:write ability on the calling token.

We deliberately don't expose a "delete by filter" tool — agents must enumerate IDs first via list_submissions, then commit. That keeps the destructive surface area visible in the agent's tool-call log.

How files cascade

When you bulk-delete:

  1. Submission rows are deleted in a single transaction.
  2. A background job sweeps any associated file records.
  3. The file bytes are removed from private storage within 24 hours.

There's no orphaned-file scenario: the delete job is idempotent and retries on transient storage failures. Force-delete on a parent form does the same thing, but synchronously.

For mark_spam / mark_not_spam, files don't move. They stay attached to the submission, which now lives in the other folder. Signed URLs work the same regardless of folder.

What's next