Documentation
Loop Desk is an always-on business workspace that takes incoming signal and turns it into briefs, watch items, drafts, and next actions. One calm place instead of twelve noisy tabs.
New to Loop Desk? Follow the Quick Start to connect your first signal source and see what the desk surfaces by tomorrow morning.
Quick Start
- Sign up at loopdesk.space. You get one workspace on the free plan.
- Log your first signal - paste in a competitor observation, a customer complaint, or a market shift using the signal form on your desk.
- Let the loop run - the desk starts processing immediately. You can also trigger a manual cycle with the "Run one cycle now" button.
- Check your queue - within minutes you will have a brief or watch item ready for review in the Approvals panel.
Signal Sources
Signal sources are the inputs that feed your desk. Anything arriving at your business can become a signal:
- Manual notes - paste or type anything directly into the desk. Available now.
- Website - connect a business site so the desk tracks context about it.
- Shopify - register your store as a source and log order signals manually until the live integration ships.
- Etsy, Notion, Google Drive - register as a source to keep the desk informed of what each tool holds.
- Feedback inbox - connect a support email or ticket source.
Automatic signal intake is live: RSS / Atom feeds, review sites (G2, Trustpilot, Google Reviews via RSS), LinkedIn (via rss.app/fetchrss bridge), inbound webhooks from Zapier/Make/scripts, and email forwarding through Mailgun/Postmark/SendGrid. Token-based authentication, optional HMAC signing.
Workspaces
A workspace is one instance of Loop Desk focused on a specific context. You might have one workspace for your primary business, one for a side project, and one for industry research.
Each workspace has its own signal sources, loop configuration, approval queue, and memory. The free plan includes 1 workspace; Pro gives you 5.
The Loop
The loop is the continuous process that runs inside each workspace. It does four things:
- Read - ingest new signals as they arrive
- Cluster - group related signals (e.g., two support threads about the same issue)
- Summarize - distill clusters into concise briefs
- Prioritize - surface what matters based on patterns and operator memory
The loop runs continuously. You do not need to trigger it manually. When you open your desk, the queue is already populated with processed results.
Approval Queue
Everything the loop produces enters the approval queue. Nothing leaves the system without your explicit sign-off.
Each item in the queue can be moved to one of three states:
- Approve - mark the output as accepted and actionable
- Ready - keep it visible in the queue for a second look
- Archive - dismiss it from active review without deleting
For outputs that are 90% right but need a small tweak, the Edit chip on every approval-queue item opens an inline editor for title, summary, and body (markdown supported). Saving updates the artifact in place and writes an activity-log line β for fundamentally different output use Try again instead, which archives the current artifact and re-queues the underlying task (rev 23).
On the task side, you can pin urgent work to a dedicated Pinned tasks panel that always sorts first regardless of priority or due date. Tasks with a due date within the next 4 hours also fire a Slack reminder (and the task.due_soon outbound event) once per due-window, so deadline-bound work never silently slips (rev 23).
Keyboard shortcuts: press ? to open the shortcut overlay, / to focus search, βK (or Ctrl+K) for the command palette (rev 27), and g followed by a / t / s / m / h to jump to Approvals, Active work, Recent signal, Memory, or Desk health (rev 23).
Task comments (rev 26) on every active-work card give teammates a thread that does not feed the AI β distinct from operator notes (rev 14), which are read by the next cycle as authoritative direction. From rev 27, type @<name> in a comment to ping a workspace member via Slack and email (and fire the task.mentioned outbound event). Type @desk in a comment to bridge that comment into authoritative AI direction on the next cycle β the comment stays in the teammate thread but also lands as an operator note. The bridge is one-way and explicit; the AI never sees regular comments. From rev 28, click Reply on any top-level comment to start a one-level thread, and Edit on your own comment within 10 minutes of posting to fix a typo; owners and admins can edit any comment. Workspace search now also reaches comments, and a Tag insights sidebar panel surfaces the top tags across tasks, outputs, and memory in the last 14 days. From rev 29, every comment also has a five-emoji reaction bar (π / π / π― / β€οΈ / π) so a teammate can ack βon itβ without writing a full reply, and the rev-17 workspace search now also reaches the activity log.
Team focus
From rev 29, owners and editors can pin up to 3 focus tags as the team's priority theme for the week. Set them from the dashboard's βTeam focusβ sidebar panel; suggestions are pre-populated from your most-used recent tags.
Once focus tags are set the dashboard queue layers a focus-tag boost between the needs_input tier and the due-date weight, so tasks tagged with a focus tag float above off-theme work without manual priority bumps. Tasks that match wear a small β
focus ribbon. The pulse engine reads the same tags β task selection inside the AI cycle agrees with what you see, and the AI prompt receives a TEAM FOCUS THIS WEEK line that biases retrieval and recommendations toward those themes.
Endpoint: PUT /api/workspace/focus-tags with { tags: string[] } (β€3, lowercase letters/numbers/hyphens).
Operator Memory
The desk builds operator memory over time. Patterns from previous signals, your approval decisions, and accumulated context carry forward so tomorrow's queue starts with history, not a blank slate.
Memory means the desk gets better at prioritization the longer you use it. A competitor price change means more when the desk has six weeks of pricing history to compare against.
Free plan retains 7 days of memory. Pro retains 90 days. Team is unlimited.
Signal Types
When adding a signal you choose one of six types. The desk uses the type to decide what kind of task and deliverable to create:
- Order movement (
order) β changes in demand, fulfilment, or transaction volume. Produces a decision artifact. - Customer feedback (
feedback) β support threads, reviews, complaints, or praise. Produces a draft response or internal note. - Competitor move (
competitor) β pricing changes, product launches, or positioning shifts. Produces a decision artifact. - Market shift (
market) β industry news, macro trends, or supply-side changes. Produces a note. - Research (
research) β articles, analyses, or reference material you want the desk to absorb. Produces a note. - Internal note (
internal) β anything from inside the business that needs processing: meeting notes, ideas, observations. Produces a note.
Output Types
The loop produces five types of output, all held in your approval queue until you act on them:
- Brief - a loop summary covering what was processed, the active task, and the recommended next action
- Draft - a suggested response or communication ready for your edits before sending
- Decision - a structured recommendation on a specific question, with supporting context
- Watchlist - a pattern or trend worth monitoring over time, generated from recurring signal
- Note - a distilled summary of research, market signal, or internal observations
Programmatic API
Every workspace has a JSON API at /api/v1. Authenticate with the workspace ingest token (Integrations panel β Inbound webhook). Pass it as a Bearer header:
curl -H "Authorization: Bearer <ingest-token>" \
https://loopdesk.space/api/v1/signals?limit=10Endpoints:
GET /api/v1/workspaceβ workspace profile and effective loop intervalPATCH /api/v1/workspace/loopβ set desk state. JSON:{ loopState: 'on' | 'paused' | 'off' }(rev 82 β closes the rev-1 desk-state primitive's longest-outstanding v1 parity gap)PATCH /api/v1/workspace/pause-untilβ schedule auto-resume pause. JSON:{ pauseUntilAt: ISO | null }(max 14d). rev 82 β mirrors the rev-21 dashboard scheduled-pause primitivePATCH /api/v1/workspace/loop-intervalβ set per-workspace loop interval. JSON:{ loopIntervalMinutes: number(1-1440) }. rev 140 β closes a long-outstanding configuration gap. Plan-tier floor (free=15, pro=5, team=1) layered server-side; response carries both requested + effective values.GET /api/v1/signals?limit=&since=β recent signals (filterable by date)POST /api/v1/signalsβ create a signal (same payload as the inbound webhook)GET /api/v1/artifacts?status=ready&kind=draftβ outputs (filterable)GET /api/v1/tasks?status=queued,in_progressβ active queue (filterable)POST /api/v1/tasksβ create a manual task β JSON:{ title, summary, kind?, deliverableType?, priority?, dueAt?, tags? }PATCH /api/v1/tasks/{id}β update task status (queue / re-queue / mark done)PUT /api/v1/tasks/{id}/tagsβ replace a task's tagsGET /api/v1/tasks/{id}/comments?q=&authorId=β list teammate-to-teammate comments on a task. Optionalqkeyword filter +authorIdauthor scope. Whenqis supplied, the response also carriesmatchedIds(ordered list of comment ids whose body OR author name contains the query) so MCP hosts render the rev-131 N/M match counter chip + ββ arrow-key navigation by index without re-running the matcher (rev 132 β closes the named rev-131 next-sprint candidate at the v1 axis). Every comment row + every distinct-author entry also carriesauthorInitials+authorHueso MCP hosts render the rev-130 dashboard avatar primitive without re-implementing the helpers (rev 26 + rev 129 + rev 131 + rev 132)POST /api/v1/tasks/{id}/commentsβ post a comment β JSON:{ text, asUserId?, parentCommentId? }. Response carriescomment.authorInitials+comment.authorHueso callers chaining create-then-render don't have to re-fetch the listing endpoint to pick up the avatar fields (rev 26 + rev 131)GET /api/v1/tasks/{id}/notesβ list operator notes on a task (rev 76 β closes the v1 parity gap on the rev-14 operator-notes primitive)POST /api/v1/tasks/{id}/notesβ post an operator note β JSON:{ text }(β€600 chars). Fed to the next AI cycle as authoritative direction; auto-promotesneeds_inputβqueued. The most direct AI-direction primitive on the desk (rev 76 β mirrors the rev-14 dashboard endpoint)GET /api/v1/sourcesβ connected signal sources. Each row carries the rev-16 health diagnostics (lastSuccessAt/lastErrorAt/lastErrorMessage) and the rev-141pollIntervalMinutesoverride (null = follow workspace cadence; integer = per-source override in minutes). The rev-26 keyword filter lives insideconfigJSONB.POST /api/v1/sourcesβ add a source (RSS, review site, LinkedIn, manual, etc.)POST /api/v1/sources/previewβ synchronously validate a feed URL before issuing the create call (rev 40)GET /api/v1/workspace/tag-search?tag=β¦&limit=25β cross-entity drill-down for a tag (rev 40 β closes the rev-39 v1 parity gap)POST /api/v1/workspace/tags/renameβ rename / merge a tag across the workspace. JSON:{ from, to }(rev 40)GET /api/v1/tasks/{id}/timeline?limit=60β per-task unified timeline (creation + AI cycles + operator notes + comments + activity) (rev 41)GET /api/v1/tasks/{id}/sourcesβ per-task input transparency: signals that originated this in-flight task (rev 43)GET /api/v1/tasks/{id}/source-memoryβ per-task memory transparency: memory entries the AI cycle pulled for this task (rev 44 β closes the rev-43 named follow-up)GET /api/v1/artifacts/{id}/sourcesβ AI-cycle inputs that shaped this artifact: signals + memory entries the cycle saw (rev 41)GET /api/v1/artifacts/{id}/versionsβ full version chain for an artifact: walks the rev-20 parentArtifactId chain (rev 44 β mirrors the rev-20 dashboard endpoint)POST /api/v1/workspace/importβ append memory / signals / sources from an export JSON. JSON:{ data, categories? }(rev 41)GET /api/v1/workspace/summaryβ all-time procurement-friendly counts (signals / artifacts by status / memory / sources by status / tasks by status / cycles / approved artifacts / anchor timestamps) (rev 43)GET /api/v1/workspace/todayβ today snapshot: signals / cycles / outputs / approvals / spend in workspace timezone, plus yesterday delta + 7-day baseline (rev 45 β mirrors the rev-33 dashboard Today panel)GET /api/v1/workspace/whats-new?since=ISOβ what landed since timestamp X: per-kind counts (signals/artifacts/outputs/approvals/activity) + small samples in one call. Defaults to last 24h, clamped to 30-day max lookback (rev 77 β bearer-auth mirror of the rev-76 dashboard WhatsNewBadge)GET /api/v1/workspace/personal-inbox?userId=β¦β per-user actionable inbox: unacked self-mentions + per-recipient stale assigned tasks + due-soon-and-mine. Defaults userId to workspace owner (rev 77 β mirrors the rev-77 dashboard PersonalInboxPill)GET /api/v1/openapi.jsonβ machine-readable OpenAPI 3.1 spec describing the most load-bearing v1 endpoints with typed request/response schemas. Public endpoint, no auth required to read (the spec describes the auth model itself). Curated subset; full coverage tracks rev-by-rev (rev 78 β opens the v1 surface to typed code generation)GET /api/v1/roadmap-votesβ public roadmap upvote counts + 14-day per-day trend + 7-day momentum per item, plus a top-5 most-requested ranking and a top-5 most-trending ranking (by trailing-7-day momentum, mutually exclusive of mostRequested). No auth β public marketing surface (rev 98 β closes the named rev-97 next-sprint candidate; rev 99 β adds momentum7d + mostTrending alongside the existing cumulative ranking)GET /api/v1/roadmap-itemsβ public roadmap content (phases + items with title, description, status, key) as JSON. Pairs with /roadmap-votes as the machine-readable companion to the public /roadmap page. No auth (rev 98)GET /api/v1/changelog?limit=20&sinceRev=Nβ public changelog releases (newest-first, rev label + date + title + highlights). MCP hosts + procurement teams can read βwhat shipped recentlyβ without scraping /changelog HTML. Pairs with /roadmap-items (what's planned) and /roadmap-votes (what's most-requested) as the three axes of the public marketing surface.sinceRevreturns only releases newer than the supplied rev number β useful for cadence polling. No auth (rev 100/101)GET /api/v1/blog?limit=20&tag=β¦&sinceDate=ISOβ public blog posts (newest-first). Optionaltag(case-insensitive substring match) andsinceDate(ISO date) filters. Closes the four-axis public marketing surface on the protocol-bound side: items (planned), votes (most-requested), changelog (shipped), blog (brand voice). No auth (rev 102)GET /api/v1/blog/{slug}β single blog post detail with full HTML body + tags + read-time + word-count + 3 related posts via tag overlap so MCP hosts can render βyou might also likeβ without a follow-up call. Pairs with /api/v1/blog (listing) as the two-axis blog read surface. No auth (rev 103)GET /api/v1/blog/categoriesβ public blog category taxonomy (key, name, color, description, postCount) for every category with at least one post. Closes the third axis on the v1 blog cluster (listing + detail + categories). No auth (rev 104)GET /api/v1/blog/authorsβ public blog author taxonomy (slug, name, postCount, latestDate, url, rssUrl). Closes the fourth axis on the v1 blog cluster (listing + detail + categories + authors). Pairs with the per-author archive page/blog/author/[slug]+ per-author RSS feed/blog/author/[slug]/rss.xml. No auth (rev 105)GET /api/v1/blog/by-author/{slug}β posts by a specific author, newest-first. Returns 404 when the slug doesn't match. MCP hosts rendering βposts by Steveβ or per-byline weekly roundups don't need to fetch /api/v1/blog and filter client-side. No auth (rev 105)GET /api/v1/blog/{slug}/neighborsβ chronological prev/next: returns{ newer, older }walking the publication timeline. Distinct from/related/{slug}(tag-overlap content similarity). Either side may be null at the head/tail. Closes the sixth axis on the v1 blog cluster (listing + detail + categories + authors + related + neighbours). No auth (rev 107)GET /api/v1/blog/tagsβ public blog tag taxonomy (slug, name, postCount, latestDate, url). Tags collapsed by slug. Closes the v1 blog cluster's seventh axis after listing + detail + categories + authors + related + neighbours. Pairs with the rev-108 per-tag HTML archive page/blog/tag/[slug]. No auth (rev 108)GET /api/v1/blog/by-tag/{slug}?limit=20β posts tagged with a specific tag, newest-first. Includes asummaryblock with totalWords + estimatedMinutes so callers don't have to re-aggregate per-post wordCount. Returns 404 when the slug doesn't match. MCP hosts rendering βposts tagged MCPβ or per-topic weekly digests don't need to fetch /api/v1/blog and filter client-side. No auth (rev 108)GET /api/v1/workspace/exportβ full workspace JSON export (workspace profile, sources, signals, tasks, artifacts, memory, recent runs/activities, plus rev-43/44 source-evidence appendices). Tokens + share metadata scrubbed (rev 45 β mirrors the rev-6 dashboard endpoint)GET /api/v1/decisions?windowDays=30&limit=10β decisions log: last N approved/archived non-brief artifacts in the trailing window (rev 46 β closes the rev-9 dashboard sidebar v1 parity gap, 37 revs old)GET /api/v1/outbound/deliveries?limit=20β recent outbound webhook delivery attempts (rev 46 β mirrors the rev-19 dashboard delivery log)POST /api/v1/outbound/deliveries/{id}/retryβ re-POST a stored payload from the delivery log (rev 46 β mirrors the rev-19 dashboard retry button)GET /api/v1/outbound/subscriptionsβ list per-event outbound subscriptions + the full ALL_OUTBOUND_EVENTS vocabulary (rev 81 β closes the rev-19 subscription-management v1 parity gap)POST /api/v1/outbound/subscriptionsβ create a subscription. JSON:{ url, events: OutboundEvent[], label? }PATCH /api/v1/outbound/subscriptions/{id}β pause/resume + edit event set. JSON:{ active?, events? }DELETE /api/v1/outbound/subscriptions/{id}β remove a subscriptionPOST /api/v1/outbound/subscriptions/{id}/testβ send anartifact.testpayload to a specific subscription URL with HMAC signing (rev 83 β closes the per-subscription test gap left by the rev-17 single-URL test)POST /api/v1/workspace/digest-previewβ send a one-shot digest preview to a workspace member. JSON:{ userId? }. Defaults to workspace owner. Recipient must be an owner/admin. Bypasses the per-workspace 22h cron interval gate so MCP hosts can iterate on configuration repeatedly. Honours the recipient's rev-78/79/80/81 dashboardPrefs (digest opt-out / weekend mute / quiet hours window) exactly as the production cron would (rev 86 β closes the dashboard-only digest-test gap from rev 36)GET /api/v1/workspace/digest-preview?userId=&format=html|jsonβ dry-run companion to the rev-86 POST. Renders the same HTML the POST endpoint would send, but doesn't actually send the email and doesn't write the activity-log entry. The Resend precondition is also skipped so workspaces withoutRESEND_API_KEY/EMAIL_FROMcan still verify the rendered shape.format=htmlreturns the raw HTML body;format=json(default) wraps the HTML inline with recipient + subject. Lets MCP hosts pipe the digest body through their own preview tool without an email round-trip (rev 87 β closes the named rev-86 next-sprint candidate)GET /api/v1/workspace/digest-configβ aggregate digest configuration snapshot. Returns workspace-level digest config (digestEmail bool + IANA timezone) + every owner/admin recipient's per-recipient dashboardPrefs (digestPersonalSections / digestQuietWeekends / digestQuietHoursStart-End) in one bearer-auth call. Pure read-only. Closes the procurement-friendly aggregate read surface for the rev-80/81/89 digest gating cluster β three round trips (workspace + members + per-recipient prefs) collapse to one (rev 90)GET /api/v1/workspace/digest-audit?sinceDays=30&limit=100β historical audit log of every digest gating change for the workspace. Reads the rev-90digest_gating_changeactivity-log entries that fire whenever a recipient's would-send outcome flips. Each entry is structured: id, createdAt, raw detail, parsed outcome (would_send|muted), and recipient label. Bounded query window (default 30 days, max 365). Pairs with rev-90 digest-config (current snapshot) + rev-89 digest-recipients-gating (current-instant outcome) + rev-86 digest-preview (render-only test) as the four-axis digest-config instrument cluster (rev 92 β closes the named rev-91 next-sprint candidate)POST /api/v1/tasks/{id}/share-work-logβ mint or revoke a per-task work-log share token. JSON:{ enable: boolean }. Returns{ ok, publicWorkLogToken | null }. Mirrors the rev-10 artifact share endpoint shape on the task entity. The token resolves through the public/share/work-log/<token>page which renders the rev-12 work log + rev-14 operator notes as a procurement-friendly evidence trail. Pairs with the rev-92 print stylesheet for the public takeaway PDF (rev 93 β closes the named rev-92 next-sprint candidate)POST /api/v1/tasks/{id}/share-timelineβ mint or revoke a per-task timeline share token. JSON:{ enable: boolean }. Returns{ ok, publicTimelineToken | null }. Distinct from/share-work-logβ the rev-93 work-log share covers AI cycle output + operator notes only; this richer surface adds the rev-26 comments + the creation event so an external reader sees the complete narrative of how the team and the desk worked the task end-to-end. Resolves through the public/share/timeline/<token>page (rev 94 β closes the named rev-93 next-sprint candidate)GET /api/v1/tasks/{id}/work-log-viewsβ trailing 14-day daily-views sparkline data for a task's public work-log share page. Returns{ days: { date, count }[] }oldest-first with zero-fill so the array length is stable for sparkline rendering. Mirrors the rev-13/api/artifacts/{id}/viewsshape so engagement-tracking parity holds across both share entities (rev 94 β closes the named rev-93 next-sprint candidate)PATCH /api/v1/sources/{id}β mutate a single source. JSON:{ status: 'pending' | 'connected' | 'error' }(rev 6 pause/resume) OR{ label: stringβ€80 }(rev 95 rename) OR{ pollIntervalMinutes: int(1-1440) | null }(rev 141 per-source poll-cadence override; null reverts to workspace cadence). Mutually exclusive payloads β exactly one field per request. The RSS poller honourslastSyncedAt + pollIntervalMinutesper source so a daily-published feed runs on a slower cadence than the workspace loop interval (rev 140) without dragging the rest of the workspace down (rev 141 β closes the named rev-140 next-sprint candidate)POST /api/v1/sources/bulk-renameβ bulk source rename / find-and-replace across one or many source labels. JSON:{ findText, replaceText, sourceIds?, caseSensitive?, preview? }. Passpreview: trueto return matches without applying. Closes the named rev-95 next-sprint candidate by collapsing N inline-rename clicks into one operation. Activity log records each per-source rename + a single bulk summary (rev 96)GET /api/v1/workspace/sources-export?format=csv|jsonβ sources catalogue with rev-16 health columns + rev-26 keyword filter columns + rev-12 7-day signal-rate column. Default CSV. Passformat=jsonfor the same rows as a typed JSON payload with includeKeywords/excludeKeywords as arrays (rev 96; rev 121 added JSON variant β closes the v1 export JSON symmetry across the six-axis procurement-evidence cluster)GET /api/v1/workspace/activity-export?since=ISO&until=ISO&format=csv|jsonβ workspace activity log as CSV (default) or JSON (format=json). Mirrors the rev-7 dashboard endpoint. Optional date-range bounds. Capped at 5000 rows. JSON variant returns{ rows, total, since, until, capped }so MCP hosts can pipe audit history through analytics pipelines without parsing CSV (rev 120; rev 121 added JSON variant β closes the named rev-120 next-sprint candidate)GET /api/v1/workspace/artifacts-export?format=csv|jsonβ workspace artifact catalog. CSV columns: createdAt, updatedAt, kind, status, title, summary, tags, share URL, view count. JSON projectstagsas an array andpublicShareUrlas a relative path. Mirrors the rev-22 dashboard endpoint (rev 120; rev 121 added JSON variant)GET /api/v1/workspace/decisions-export?format=csv|jsonβ decisions log as CSV (default) or JSON. Approved/archived non-brief artifacts scoped exactly to the rev-9 dashboard semantics. Distinct from/api/v1/decisions(last 10 in 30 days for the dashboard sidebar) β the export shape is the procurement takeaway across the full 5,000-row cap. Mirrors the rev-47 dashboard endpoint (rev 120; rev 121 added JSON variant)GET /api/v1/workspace/stale-tasks-export?thresholdDays=N&format=csv|jsonβ stale-task CSV (default) or JSON with rev-51 cost columns inline. Mirrors the rev-50 dashboard endpoint with the same 1-60 threshold override. The procurement-evidence v1 mirrors are now complete across all six axes (activity / outputs / decisions / stale-tasks / cost-summary / sources) AND every axis can be requested in JSON or CSV β closes the v1 export JSON symmetry on the protocol-bound surface (rev 120; rev 121 added JSON variant)GET /api/v1/workspace/cost-export?format=csv|jsonβ cost summary takeaway. Default CSV (four-section: DAILY / BY_TASK / BY_SOURCE / BY_ASSIGNEE). Passformat=jsonfor a structured object grouped by axis ({ daily, byTask, bySource, byAssignee, timezone, generatedAt }) so MCP hosts and FinOps pipelines can render the cost summary without parsing the multi-section CSV (rev 62; rev 121 added JSON variant)GET /api/v1/workspace/memory-export?format=csv|jsonβ memory entries takeaway as CSV (default) or JSON. Returns pinned + importance-ranked durable knowledge (facts / decisions / preferences / lessons) with tags inline. JSON projectstagsas an array (CSV joins by|). Closes the procurement-evidence cluster's seventh axis on the v1 surface β durable knowledge alongside activity / outputs / decisions / stale-tasks / cost / sources (rev 125)GET /api/v1/workspace/shared-saved-searchesβ list workspace-shared saved searches. Distinct from rev-18 personal localStorage saved searches β these are workspace-shared so multi-operator teams can curate a power-user search vocabulary that surfaces in every member's WorkspaceSearch dropdown (rev 93)POST /api/v1/workspace/shared-saved-searchesβ add a workspace-shared saved search. JSON:{ name, query, asUserId? }. Defaults attribution to workspace owner. Capped at 30 entries per workspace (rev 93 β closes the named rev-92 next-sprint candidate)DELETE /api/v1/workspace/shared-saved-searchesβ remove a workspace-shared saved search. JSON:{ searchId }(rev 93)POST /api/v1/outbound/subscriptions/bulkβ bulk pause/resume/delete on outbound subscriptions. JSON:{ action: 'pause' | 'resume' | 'delete', subscriptionIds: string[β€50] }. Closes the five-entity bulk-action symmetry on the outbound subscription axis (rev 93)POST /api/v1/cost/by-tag/chronic-ack/bulkβ bulk acknowledge per-tag chronic warnings for 7 days. JSON:{ tags: string[β€50] }. Distinct from/cost/by-tag/cost-spike-ack/bulk(rev 68 β daily horizon). Closes the chronic-axis bulk-action symmetry at the per-tag dimension (rev 87)POST /api/v1/sources/chronic-ack/bulkβ bulk acknowledge per-source chronic warnings for 7 days. JSON:{ sourceIds: string[β€50] }. Distinct from/sources/cost-spike-ack/bulk(rev 60 β daily horizon). Closes the chronic-axis bulk-action symmetry at the per-source dimension (rev 87)POST /api/v1/cost/by-assignee/chronic-ack/bulkβ bulk acknowledge per-assignee chronic warnings for 7 days. JSON:{ assigneeUserIds: string[β€50] }. Distinct from/cost/by-assignee/cost-spike-ack/bulk(rev 63 β daily horizon). Closes the chronic-axis bulk-action symmetry at the per-recipient dimension β the chronic bulk-action loop is now closed on every dimension where chronic makes sense (rev 87)GET /api/v1/workspace/membersβ list workspace members + pending invites (rev 81 β closes the longest-outstanding v1 parity gap on the rev-15 invite flow, 66 revs old)POST /api/v1/workspace/invitesβ invite a teammate by email. JSON:{ email, role?: "admin"|"editor"|"viewer", asUserId? }.asUserIddefaults to workspace ownerDELETE /api/v1/workspace/members/{userId}β remove a member. Workspace owners cannot be removed. Body:{ asUserId? }(optional)DELETE /api/v1/workspace/invites/{inviteId}β revoke a pending inviteGET /api/v1/tasks/stale?thresholdDays=5&limit=10β stale tasks (queued/in_progress for >thresholdDays without an updatedAt bump; pinned excluded) (rev 48 β mirrors the rev-47 dashboard StaleTasksPanel)GET /api/v1/memory/stale?thresholdDays=30&limit=12&includeNeverRetrieved=true&tag=β¦β stale memory entries the AI cycle hasn't pulled in N days. Pinned + importanceβ₯9 entries excluded server-side. Each row carrieslastRetrievedAt,retrievalCount,daysSinceRetrieved,daysSinceCreated. As of rev 156, optional?tag=filter via JSONB@>array containment for per-workstream scoping (rev 153 + rev 156)GET /api/v1/memory/{id}/retrieval-trajectory?days=7β per-memory daily retrieval trajectory (1β30 days, default 7) in workspace timezone, oldest β newest, with zero-fill. ReadsmemoryEntries.retrievalHistorythe pulse engine bumps viajsonb_seton every retrieval. Mirrors the rev-54/tasks/{id}/cost-trajectoryat the per-memory axis on the trajectory dimension. Closes the *trajectory* axis on the per-memory observability cluster (rev 157 β closes named rev-156 next-sprint candidate)GET /api/v1/memory/top-retrieved?limit=5&days=7β workspace-axis read on the rev-157 trajectory primitive. Top-N memory entries by trailing-N-day retrieval count; each row carriesretrievals7d,retrievalCount, and per-rowtrajectory7darray. Mirrors the rev-51/tasks/top-costshape at the per-memory axis on the retrieval dimension. Pairs with/memory/stale(workspace-axis on staleness) and/memory/{id}/retrieval-trajectory(per-memory-axis on trajectory) for the full per-memory observability story. Also:GET /api/v1/memorylisting rows now carryretrievals7dderived field alongsideretrievalCount+lastRetrievedAt(rev 158 β closes named rev-157 next-sprint candidate)GET /api/v1/memory/cost-spikes?limit=10β live memory entries today >= 2Γ their trailing 7-day daily average AND >= $0.50 absolute. Pinned + importanceβ₯9 entries excluded server-side as load-bearing-by-design. Closes the cost-spike alarm cluster's seventh axis (workspace rev 32 / per-task rev 55 / per-source rev 58 / per-assignee rev 62 / per-tag rev 67 / per-memory rev 161). Pairs with/memory/top-cost(rev 159 cumulative) +/memory/{id}/cost-trajectory(rev 160 trajectory) as the three-axis cost-observability surface on the per-memory entity. Each row carriescostSpikeAckedAtas of rev 162 so MCP hosts render the muted state inline. Daily sweep also pushes Slack + the newmemory.cost_spikeoutbound webhook event (rev 161 β closes the cost-spike alarm cluster on the per-memory axis)POST /api/v1/memory/{memoryId}/cost-spike-ackβ acknowledge a per-memory cost spike for the rest of today (workspace TZ). Closes the rev-161 alarm-only gap on the operator counter-action surface. Mirrors the rev-56 task / rev-59 source / rev-63 assignee / rev-68 tag ack at the per-memory axis. Fires the newmemory.cost_spike_ackedoutbound event (rev 162 β closes the named rev-161 next-sprint candidate)POST /api/v1/memory/cost-spike-ack/bulkβ bulk acknowledge per-memory cost spikes. Body{ memoryIds: string[β€50] }. Mirrors/tasks/cost-spike-ack/bulkrev 57 +/sources/cost-spike-ack/bulkrev 60 +/cost/by-assignee/cost-spike-ack/bulkrev 63 +/cost/by-tag/cost-spike-ack/bulkrev 68 at the per-memory axis (rev 162 β closes the cost-spike-ack v1 cluster across all five axes)GET /api/v1/memory/chronic-warnings?limit=10β per-memory chronic warning read. Memory entries whoseconsecutiveSpikeDayscounter has crossed the chronic threshold (3 days) AND haven't been chronic-acked within the 7-day TTL. Mirrors/cost/by-source/chronic-spikes(rev 71) +/cost/by-assignee/chronic-spikes(rev 71) +/cost/by-tag/chronic-spikes(rev 70) at the per-memory axis on the chronic horizon. Closes the chronic axis on the cost-spike alarm cluster's seventh axis at parity with the daily axis (rev 161). Daily sweep also pushes Slack + the newmemory.chronic_warningoutbound webhook event (rev 163)POST /api/v1/memory/{memoryId}/chronic-ackβ acknowledge a per-memory chronic warning for the next 7 days (workspace TZ). Mirrors the rev-71 tag chronic / rev-72 source chronic / rev-72 assignee chronic at the per-memory axis. Fires the newmemory.chronic_warning_ackedoutbound event. Distinct fromcost-spike-ack(rev 162) β chronic ack lives at the structural axis with a 7-day TTL because the underlying problem is structural (rev 163)POST /api/v1/memory/chronic-ack/bulkβ bulk acknowledge per-memory chronic warnings. Body{ memoryIds: string[β€50] }. Mirrors/sources/chronic-ack/bulkrev 87 +/cost/by-tag/chronic-ack/bulkrev 87 at the per-memory axis on the chronic horizon (rev 163 β closes the chronic-ack v1 cluster across all chronic axes)GET /api/v1/tasks/auto-archived?sinceDays=30&limit=50β tasks recently auto-archived by the rev-49 sweep, identified by synthetic blocker prefix (rev 50 β audit-trail closure for the stale-task lifecycle)POST /api/v1/tasks/{id}/renewβ re-affirm a task: bump updatedAt + clear the archive warning. Resets the staleness countdown without delete-and-recreate (rev 50)GET /api/v1/tasks/{id}/costβ per-task cumulative AI cost (totalInputTokens, totalOutputTokens, estimatedCostUsd, sessionCount, costPerSessionUsd) (rev 51 β closes the rev-49 named cost-attribution follow-up)GET /api/v1/tasks/{id}/cost-trajectory?days=7β per-task daily cost buckets (1β30, default 7) in workspace timezone, oldest β newest, with zero-fill. Reads task.dailyCostHistory the pulse engine appends to on every cycle (rev 54 β closes the named rev-53 follow-up)GET /api/v1/tasks/top-cost?limit=5&includeDone=falseβ top open tasks by cumulative AI spend; each row carries a trajectory7d cents array as of rev 54 (rev 51 + rev 54)GET /api/v1/cost/by-assignee?limit=10&includeDone=false&trajectoryDays=7β per-assignee cost breakdown; aggregates per-task AI spend by assignedToUserId. As of rev 61, acceptstrajectoryDays(1-30) so each row carries atrajectory7dcents array (rev 52 + rev 61).GET /api/v1/cost/by-assignee/{assigneeId}/trajectory?days=7β per-assignee daily cost trajectory in workspace timezone, oldest β newest. Pass__unassigned__to drill into the unassigned bucket. Closes the third trajectory axis on the cost cluster: workspace (rev 53) + per-task (rev 54) + per-source (rev 60) + per-assignee (rev 61).GET /api/v1/cost/todayβ cost-only Today projection; today/yesterday/baseline7d cents, 7-day daily-spend sparkline, dailyCostCap + capPercent, plus byAssignee[] (rev 53 β closes the cost-axis MCP cluster: per-cycle/per-task/per-teammate/today)GET /api/v1/tasks/cost-spikes?limit=10β open tasks today >= 2Γ their trailing 7-day daily average AND >= $0.50 absolute. Mirrors the rev-32 workspace-level cost spike at the per-task axis using the rev-54 dailyCostHistory primitive (rev 55 β closes the named rev-54 next-sprint candidate)GET /api/v1/sources/cost-spikes?limit=10β live sources today >= 2Γ their trailing 7-day daily average AND >= $0.50 absolute. Closes the alarm axis to match the rev-57 per-source attribution. Pairs with rev-32 (workspace) + rev-55 (per-task) as the three-axis alarm cluster (rev 58)GET /api/v1/sources/cadence-overviewβ per-source poll cadence + next-poll estimate aggregate; returns workspace cadence (rev 140), plan floor, and per-source breakdown (intervalMinutes, isOverride, lastSyncedAt, nextPollAt, isDueNow, minutesUntilDue) sorted soonest-due first. Mirrors the rev-141 per-source cadence override + rev-5 RSS poller gating logic so MCP hosts can answer "when will source X poll next?" without computing it client-side (rev 142 β closes the named rev-141 next-sprint candidate at the protocol surface)GET /api/v1/sources/quietnessβ per-source quietness/staleness aggregate; returns workspace cadence floor + per-source breakdown of feeds that polled successfully but produced no signals beyond the 14-day staleness floor (or 2Γ the per-source cadence interval, whichever is greater, capped at 90d). Distinct from rev-16 loud failure, rev-58 cost-spike, and rev-12 7-day signal counter β quietness is the *prescriptive* answer to "this feed has been silent long enough that the operator should consider whether it's still load-bearing." Each row carriesquietnessAckedAt(rev 145) so callers can render muted-state. (rev 144 β closes the named rev-143 next-sprint candidate at the protocol surface)POST /api/v1/sources/{sourceId}/quietness-ackβ acknowledge a per-source quietness warning for 7 days. Suppresses the rev-144 π chip badge state, the rev-145 daily Slack push, and the rev-145 outboundsource.quietness_warningevent for 7 days unless the source produces fresh signal. Distinct from rev-6 pause (permanent until resumed), rev-26 keyword filters (per-item gating), and rev-72 chronic-ack (cost-spike axis). Quietness ack = "I see this is silent, intentionally, mute the alarm for 7 days." (rev 145 β closes the named rev-144 next-sprint candidate at the operator counter-action surface)POST /api/v1/sources/{sourceId}/quietness-unackβ revoke a per-source quietness ack before its rev-145 7-day TTL expires. Operators occasionally realise that a feed they ack-muted via the rev-145 chip is genuinely critical and want to revoke the mute *before* the rev-145 7-day TTL expires. Fires the new rev-148source.quietness_unackedoutbound event so downstream integrations that closed their alarm-open ticket on the rev-145 ack closure can re-open it on un-ack. Returns 404 when there's no ack stamp to revoke (no-op un-ack returns 404 rather than silently succeeding). The full per-source quietness lifecycle is now reachable end-to-end through MCP/v1 across visibility (rev 144) β ack (rev 145) β bulk-ack (rev 146) β un-ack (rev 148). (rev 148 β closes the un-mute gap on the rev-145 ack closure)POST /api/v1/sources/quietness-unack/bulkβ bulk revoke per-source quietness acks. JSON:{ sourceIds: string[β€50] }. Closes the un-mute symmetry on the rev-146 bulk-ack pattern. When an operator who bulk-muted N feeds via rev-146 realises one or more are actually critical, they can reverse the whole batch in one call instead of clicking the rev-148 chip on every row. Already-clean sources (no ack stamp) are silent no-ops. Fires onesource.quietness_unackedevent per actually-revoked source. The full per-source quietness lifecycle on v1 is now five axes deep: visibility (rev 144) + ack (rev 145) + bulk-ack (rev 146) + un-ack (rev 148) + bulk-un-ack (rev 149). (rev 149 β closes the inline-vs-batch un-mute symmetry)POST /api/v1/tasks/{id}/cost-spike-ackβ acknowledge a per-task cost spike for the rest of today (workspace TZ). Suppresses the rev-55 β‘ pill, daily Slack push, digest section, and rev-56 auto-pause filter for the acked task today. Distinct from rev-23 pin (permanent exempt) and rev-50 renew (resets staleness). Ack = "I see this spike, stop alarming today." (rev 56 β closes the named rev-55 follow-up)GET /api/v1/workspace/cost-spike-configβ read the workspace's per-task cost spike action:none(rev-55 alarm-only) orpause(rev-56 auto-pause)PUT /api/v1/workspace/cost-spike-configβ set the action:{ taskCostSpikeAction: "none" | "pause" | null }.pausewires the rev-55 detector into the pulse engine's selectNextTask filter so spiking tasks de-prioritise until the operator acknowledges (rev 56 β mirrors the rev-49 stale auto-archive opt-in pattern at the cost axis)POST /api/v1/sources/{id}/cost-spike-ackβ acknowledge a per-source cost spike for the rest of today (workspace TZ). Suppresses the rev-58 β‘ pill, daily Slack push, outbound source.cost_spike event, and rev-59 auto-pause filter for the acked source today. Ack = "I see this source spike, stop alarming today, but watch for tomorrow." (rev 59 β closes the named rev-58 follow-up at the per-source operator counter-action surface)GET /api/v1/workspace/source-cost-spike-configβ read the workspace's per-source cost spike action:none(rev-58 alarm-only) orpause(rev-59 auto-pause)PUT /api/v1/workspace/source-cost-spike-configβ set the action:{ sourceCostSpikeAction: "none" | "pause" | null }.pausewires the rev-58 detector into the RSS poller so spiking feeds skip the next poll cycle until the operator acknowledges (rev 59 β mirrors the rev-56 per-task auto-pause at the per-source axis)POST /api/v1/sources/cost-spike-ack/bulkβ bulk acknowledge per-source cost spikes:{ sourceIds: string[β€50] }. Mirrors the rev-57 task bulk-ack at the per-source axis (rev 60).GET /api/v1/sources/{id}/cost-trajectory?days=7β per-source daily cost trajectory in workspace timezone, oldest β newest. Mirrors rev-54 task trajectory at the per-source axis. Answers "is this source steadily expensive or just spiking today?" (rev 60)GET /api/v1/cost/by-tag?limit=10&includeDone=false&trajectoryDays=7β per-tag cost breakdown; aggregates per-task AI spend by tags so MCP hosts can answer "which workstream is eating my AI spend?". Each task contributes its full cost to every tag it carries. As of rev 67, accepts optionaltrajectoryDays(1β30) so each row carries atrajectory7dcents array β mirrors the rev-60 by-source + rev-61 by-assignee shape at the per-tag axis. (rev 66 + rev 67)GET /api/v1/cost/by-tag/{tag}/trajectory?days=7β per-tag daily cost trajectory in workspace timezone, oldest β newest, with zero-fill. Passuntaggedas the route segment to drill into the synthetic untagged bucket. Closes the trajectory dimension on every cost axis: workspace (rev 53) + per-task (rev 54) + per-source (rev 60) + per-assignee (rev 61) + per-tag (rev 67).GET /api/v1/cost/by-tag/spikes?limit=10β per-tag cost spike list; live tags whose today spend is >= 2Γ their trailing 7-day daily average AND >= $0.50 absolute. The syntheticuntaggedbucket is excluded β flagging "untagged work is spiking" isn't actionable. Each row carriesconsecutiveSpikeDaysas of rev 70 so MCP hosts can render the same chronic-axis affordance the dashboard surfaces on the rev-66 panel. Closes the alarm cluster's fifth axis (workspace rev 32 / per-task rev 55 / per-source rev 58 / per-assignee rev 62 / per-tag rev 67).GET /api/v1/cost/by-tag/chronic-spikes?limit=10β per-tag chronic-spike list; workstream tags whoseconsecutiveSpikeDayscounter has crossed 3 days. Distinct from the rev-67 daily endpoint: chronic names a structural problem (workstream over-budget, noisy source attached to the tag, stale focus tag) rather than today's anomaly. Pairs with the companiontag.chronic_warningoutbound webhook so external integrations can route the alarm without polling. Closes the chronic axis on the per-tag cost story alongside rev-67/68/69's daily-spike axis; mirrors rev-61 source counter + rev-64 assignee counter at the per-workstream axis (rev 70).GET /api/v1/cost/chronic-acksβ currently-active chronic-warning acks across all three axes (per-tag, per-source, per-assignee), filtered to the rev-71 7-day TTL. Lets MCP hosts answer "what defensive operator actions are in effect right now?" with one bearer-auth call. Each row carries the ack timestamp + the expiry timestamp + a humanised handle so an MCP host can render "currently muted: #q3-launch (5d left), Steve's queue (3d left), RSS bridge X (6d left)" without follow-up calls per axis. Pairs with the rev-71 v1 chronic-spike read endpoints, the rev-71/72 v1 chronic-ack write endpoints, and the rev-73 chronic-ack closure-receipt outbound events for the complete read/write/closure picture on the chronic surface (rev 73 β closes the named rev-72 next-sprint follow-up at the read surface).GET /api/v1/workspace/tag-cost-spike-configβ read the workspace's per-tag cost spike action:none(rev-67/68 alarm + ack only) orpause(rev-69 auto-pause: tasks tagged with a currently spiking workstream skip the queue until acknowledged) (rev 69)PUT /api/v1/workspace/tag-cost-spike-configβ set the action:{ tagCostSpikeAction: "none" | "pause" | null }.pausewires the rev-67 detector into the pulse engine's selectNextTask filter so any task whose tags overlap with a currently-spiking workstream skips the queue until acknowledged via the rev-68 ack surface (rev 69 β closes the descriptiveβdefensive loop on the per-tag cost story; mirrors rev-56 per-task + rev-59 per-source auto-pause at the per-tag axis)GET /api/v1/workspace/task-templatesβ list persistent workspace task templates the operator has saved alongside the rev-64 static set + rev-65 detected pattern (rev 66 β closes the named rev-65 next-sprint candidate)POST /api/v1/workspace/task-templatesβ save a persistent task template (workspace-scoped recurring shape):{ label, emoji?, hint?, title, summary, goal?, kind, deliverableType, priority?, tags?, asUserId? }. Capped at 20 templates per workspace; unique by label.PATCH /api/v1/workspace/task-templates/{id}β update a persistent template (partial JSON of the same shape as POST)DELETE /api/v1/workspace/task-templates/{id}β remove a persistent templatePOST /api/v1/workspace/task-templates/{id}β record-usage (idempotent counter bump). Used by the dashboard quick-start chip apply path so persistent templates get recent-first ordering without manual bookmarking (rev 66)GET /api/v1/memoryβ durable workspace memory ordered by pinned + importancePOST /api/v1/memoryβ teach the desk a fact, decision, preference, or lessonGET /api/v1/runsβ recent loop cycles with token usage and estimated costGET /api/v1/search?q=β¦β keyword search across signals, tasks, outputs, memory, comments, and activity log (rev 29)GET /api/v1/statsβ desk health score, runtime phase, 7-day cycles / signals / approvals / costGET /api/v1/activity?limit=&since=&kind=&q=β workspace activity log newest-first.qdoes an in-place keyword filter across detail + kind, mirroring the rev-38 dashboard inline activity-log search (rev 46).GET /api/v1/insights?windowDays=14&limit=8β top tags across tasks/artifacts/memory in a recent window (rev 28)GET/POST /api/v1/tasks/{id}/commentsβ list and post comments. POST accepts{ text, asUserId?, parentCommentId? }; passparentCommentIdto reply inside an existing thread (rev 28).POST /api/v1/tasks/{id}/comments/{commentId}/reactionβ toggle a comment reaction. JSON:{ emoji, asUserId? }. Emoji must be one of π / π / π― / β€οΈ / π (rev 30).GET/PUT /api/v1/workspace/focus-tagsβ read or set the workspace's up-to-3 team focus tags. PUT accepts{ tags: string[] }. The pulse cycle reads focus tags both for queue selection and memory retrieval, so MCP hosts can shift the desk's attention programmatically (rev 30).GET /api/v1/workspace/focus-history?sinceDays=90&limit=30β focus drift timeline. Returns every focus shift in the window with the tag set, when it was set, and how long it lasted before being replaced (rev 31).POST /api/v1/artifacts/{id}/reaction+POST /api/v1/memory/{id}/reactionβ toggle a lightweight ack on an output or memory entry without writing a comment. Same emoji set as comment reactions (rev 33).POST /api/v1/signals/{id}/reactionβ toggle a reaction on a signal. Closes the four-entity reaction symmetry across comments, outputs, memory, and signals (rev 34).- Bulk operations (rev 35).
POST /api/v1/tasks/bulkβ JSON:{ action: "delete"|"priority"|"tag"|"untag", taskIds: string[β€50], priority?, tag? }.POST /api/v1/signals/bulkβ JSON:{ action: "delete"|"pin", signalIds: string[β€50], pinned? }.POST /api/v1/memory/bulk-updateβ JSON:{ action: "delete"|"pin"|"tag"|"untag", memoryIds: string[β€50], pinned?, tag? }.POST /api/v1/artifacts/bulkβ JSON:{ action: "approve_all"|"archive_all" }. Caps at 50 IDs per call where applicable; mirrors the dashboard bulk semantics so MCP hosts can clear a noisy queue in one tool call.
Rotating the ingest token in the dashboard immediately invalidates old API access. The v1 API is the foundation for the upcoming Loop Desk MCP server, which will expose the same primitives to Claude Desktop, Cursor, and any MCP-compatible client.
Outbound webhooks
Loop Desk also goes the other way: configure an outbound webhook URL in the integrations panel and the desk POSTs JSON event payloads to your endpoint. Pipe into Zapier, n8n, your CRM, or any service that accepts a webhook.
- Events sent:
artifact.ready(output drafted),artifact.approved(operator approved),signal.created(manual or inbound signal landed),task.assigned(task routed to a teammate),task.due_soon(task within 4h of its due date; rev 23),task.commented(teammate posted a comment on a task; rev 26),task.mentioned(a comment includes@<teammate>and resolves to one or more workspace members; rev 27),task.mention_acked(a teammate clicked βI've got thisβ on an @-mention; rev 35),task.unblocked(a dependent task's last blocker just flipped to done and the desk is now eligible to pick it up; rev 37),task.stale_warning(one or more tasks have been queued or in-progress for more than five days without progress; daily push, rate-limited to once per workspace per 24h; rev 48),task.auto_archived(the rev-49 stale-task auto-archive sweep transitioned one or more tasks todonebecause they crossed the workspace's configured threshold; daily push),memory.auto_archived(the rev-154 stale-memory auto-archive sweep deleted one or more memory entries that hadn't been retrieved by an AI cycle in the configured threshold of days; daily push, rate-limited to once per workspace per digest cron tick; rev 154),memory.archive_warning(the rev-155 pre-archive warning sweep determined one or more memory entries are 1-2 days from rev-154 auto-archive; workspace-shared push (memory entries don't have assignees); rev 155),task.archive_warning(the rev-50 pre-archive warning sweep determined a task assigned to a teammate is 1-2 days from auto-archive; per-task push to that assignee), andworkspace.cost_cap_warning(today's spend hit 80% of the daily cost cap; rev 21),workspace.cost_spike(today's workspace spend is >= 2Γ the trailing 7-day daily average; rev 32),task.cost_spike(one or more open tasks today are spending >= 2Γ their own trailing 7-day daily average AND >= $0.50 absolute; daily push, rate-limited to once per workspace per 24h; rev 55),source.cost_spike(one or more live sources today are spending >= 2Γ their own trailing 7-day daily average AND >= $0.50 absolute; daily push, rate-limited to once per workspace per 24h; closes the alarm axis to match the rev-57 attribution; rev 58),task.cost_spike_acked(closure receipt β fires when an operator acks a per-task spike via the inline button or bulk-ack; rev 61),source.cost_spike_acked(closure receipt for the per-source variant; rev 61),assignee.cost_spike(one or more teammate queues today are spending >= 2Γ their own trailing 7-day daily average AND >= $0.50 absolute; daily push, rate-limited to once per workspace per 24h; closes the alarm cluster's fourth axis at the per-recipient level; rev 62),assignee.cost_spike_acked(closure receipt β fires when an operator acks a per-assignee spike via the inline button or bulk-ack; rev 63),assignee.chronic_warning(one or more teammate queues have been spiking 3+ days running β recommends rebalancing rather than auto-acting; daily push, rate-limited to once per workspace per 24h; closes the chronic-spike axis at the per-recipient level; rev 64),tag.cost_spike(one or more workspace tags / workstreams today are spending >= 2Γ their own trailing 7-day daily average AND >= $0.50 absolute; daily push, rate-limited to once per workspace per 24h; closes the alarm cluster's fifth axis at the per-tag / per-workstream level; rev 67),tag.cost_spike_acked(closure receipt β fires when an operator acks a per-tag spike via the inline button or bulk-ack; closes the alarm cluster's fifth axis on the closure-receipt loop so external integrations can reconcile alarm-open with alarm-acknowledged without polling the dashboard; rev 68),tag.chronic_warning(one or more workspace tags / workstreams have been spiking 3+ consecutive days running β recommends a structural change (drop the focus tag, source filter, scope down, raise cap) rather than auto-acting; daily push, rate-limited to once per workspace per 24h; closes the chronic axis on the per-tag cost story alongside the rev-67/68/69 daily-spike axis; mirrors rev-61 source counter + rev-64 assignee counter at the per-workstream axis; rev 70),tag.chronic_warning_acked/source.chronic_warning_acked/assignee.chronic_warning_acked(closure receipts β fires when an operator acks a chronic warning for 7 days via the rev-71 / rev-72 chronic-ack chip; closes the rev-37 closure-receipt pattern at the chronic-ack axis on every dimension where chronic makes sense, mirroring the rev-61 task / rev-61 source / rev-63 assignee / rev-68 tag cost-spike-acked at the chronic horizon (7d mute) rather than the daily horizon (per-day mute); rev 73),source.chronic_warning(one or more sources have been spiking 3+ consecutive days running; closes the chronic-warning push parity gap with per-tag (rev 70) and per-assignee (rev 64); recommends adding a keyword filter, permanently pausing the feed, or removing the source rather than auto-acting; daily push, rate-limited to once per workspace per 24h; rev 74),source.chronic_resumed(closure receipt β fires when an operator resumes a source the desk auto-paused via the rev-62 chronic-spike auto-pause sweep; closes the rev-37 closure-receipt pattern at the chronic auto-pause axis so downstream integrations watchingsource.chronic_auto_pausedcan close their open βalarmβ tickets when the operator clears the underlying issue; rev 74), andmemory.promoted(rev 32),digest.gating_changed(closure receipt β fires when an owner/admin recipient updates their dashboardPrefs (digestQuietWeekends / digestQuietHoursStart-End) in a way that flips todayβs would-send outcome; closes the rev-37 closure-receipt pattern at the digest gating axis; rev 90; rev 91 adds matching Slack + per-recipient email push alongside the outbound webhook so the gating change reaches every operator-loaded channel β outbound (integrations), Slack (workspace audit), email (per-recipient inbox)),source.quietness_warning(one or more sources have polled successfully but produced no signals beyond the 14-day staleness floor; closes the chronic-quietness alarm channel at parity with rev-58 cost-spike + rev-74 chronic-warning; daily push, rate-limited to once per workspace per 24h; ack-muted sources within the rev-145 7-day TTL are excluded from the push; rev 145 β closes the named rev-144 next-sprint candidate),source.quietness_acked(closure receipt β fires when an operator clicks the rev-145 ack chip beside the rev-144 π quiet pill; mirrorssource.chronic_warning_ackedat the structural-quietness axis; rev 145),source.quietness_unacked(symmetric un-mute closure receipt β fires when an operator revokes a rev-145 quietness ack via the new rev-148 un-ack chip before the 7-day TTL expires; lets external integrations that closed their alarm-open ticket on the rev-145 ack closure re-open it on un-ack rather than polling; rev 148),source.pause_until_warning(per-source scheduled pause-until daily push β fires from the daily sweep when 1+ sources havepausedUntilAtset + still in the future, payload mirrors the rev-151 GET /sources/scheduled-paused response shape; distinct fromsource.quietness_warningβ pause-until is operator intent (you scheduled this), so the warning reads as a forward-looking lifecycle preview rather than a structural alarm; rev 152 β closes the named rev-151 next-sprint candidate at the chat-channel axis),memory.cost_spike(one or more memory entries today are spending >= 2Γ their trailing 7-day daily average AND >= $0.50 absolute; closes the cost-spike alarm cluster's seventh axis β workspace (rev 32) / per-task (rev 55) / per-source (rev 58) / per-assignee (rev 62) / per-tag (rev 67) / per-memory (rev 161); daily push, rate-limited to once per workspace per 24h; pinned + importanceβ₯9 entries excluded as load-bearing-by- design; rev 161 β closes the cost-spike alarm cluster on the per-memory axis), andmemory.cost_spike_acked(closure receipt β fires when an operator acks a per-memory cost spike via the rev-162 inline ack chip or bulk-ack on TopCostMemoryPanel; closes the alarm cluster's seventh axis on the closure-receipt loop so downstream FinOps integrations can reconcile alarm-open with alarm-acknowledged at the per-knowledge- entity axis just as they already could at task / source / assignee / tag; rev 162 β closes the named rev-161 next-sprint candidate),memory.chronic_warning(one or more memory entries have been spiking 3+ days running β fires from the daily sweep with the rev-163 chronic-warning sub-sweep; mirrorstag.chronic_warningrev 70 +assignee.chronic_warningrev 64 +source.chronic_warningrev 74 at the per-memory axis on the cost dimension's chronic horizon; rate-limited via its own activity-log kind so the chronic ping doesn't drown out the rev-161 daily β‘ alarm; rev 163 β closes the chronic axis on the cost-spike alarm cluster's seventh axis at parity with the daily axis), andmemory.chronic_warning_acked(closure receipt for the rev-163 chronic-warning alarm β fires when an operator acks a per-memory chronic warning via the rev-163 β³ ack chip or bulk-ack on TopCostMemoryPanel; mirrorstag.chronic_warning_ackedrev 73 +source.chronic_warning_ackedrev 73 +assignee.chronic_warning_ ackedrev 73 at the per-memory axis on the chronic-axis closure-receipt loop; rev 163).artifact.testis fired by the βSend testβ button. - Per-event subscriptions (rev 19): in addition to the legacy single-URL field, you can register multiple subscriptions in the integrations panel β each scoped to a subset of events. Use this to route
signal.createdto your CRM,artifact.approvedto a wiki, andtask.assignedto a project tool. - Delivery log + retry (rev 19): every POST attempt is recorded with status, HTTP code, and reason. The integrations panel surfaces the last 20 attempts; failed deliveries can be replayed in one click.
- If webhook signing is enabled, the request includes
X-Loop-Signature: sha256=β¦over the raw body β the same secret used for inbound. - Failures are logged to the workspace activity trail; delivery is best-effort and never blocks the loop.
- Use the βSend testβ button in the integrations panel to verify connectivity before relying on it.
Public health badge
GET /api/v1/badge.svg?token=<ingest> returns a live shields.io-style SVG showing your desk health score and label. Embed in a README, a status page, or a Notion doc. Cached for five minutes; the token in the URL is the same workspace ingest token used elsewhere.
Public stakeholder feedback
Every /share/<token> page now lets viewers send a reaction (looks good / raise a concern / comment) back to the desk as a fresh feedback signal. Anyone with the share link can submit; concerns are flagged at high priority and trigger the same outbound signal.created webhook as any other signal. Closes the bidirectional loop on shared briefs.
Governance & ISO 42001
Loop Desk is built around an approval-first architecture: nothing leaves the desk without explicit human sign-off. That model maps directly to the human-in-the-loop controls procurement teams now ask for under ISO/IEC 42001 (AI Management System) and SOC 2 vendor reviews. This section is for procurement, security, and compliance teams evaluating Loop Desk against governance requirements.
Approval boundary (the one-way control)
- Every output produced by the loop lands in draft or ready status.
- External actions (sends, posts, decisions) are not wired to artifact creation.
- Status only advances to approved when a workspace member explicitly clicks Approve.
- The Slack brief push is a summary β it never executes outputs on behalf of the operator.
Audit trail (what gets recorded)
- Activity log β every signal capture, task transition, artifact status change, and integration event is appended to
desk_activitywith a timestamp. Exportable as CSV (up to 5,000 rows) at/api/workspace/activity-export. - Decisions log β approvals and archives within the last 30 days are surfaced in-app as a permanent record.
- Reviewer notes β when an approver leaves a note, it is stored both as a
preferencememory entry and appended to thedecisionaudit memory. - Loop history β every cycle (manual, daemon, cron) writes a
desk_runrow with trigger, status, summary, and token spend. - Per-task work log β every cycle that worked on a task appends a timestamped entry to that taskβs work log; visible in-app.
Data control
- Workspace JSON export β
/api/workspace/exportreturns a downloadable archive (Slack URLs, ingest tokens, signing secrets, and public share tokens are scrubbed). - Activity CSV / JSON export β
/api/workspace/activity-exportreturns the activity log as CSV (default). Optional?since=ISO&until=ISOquery params scope the export to a specific quarter / month for SOC 2 / ISO 42001 reviewers needing date-bounded evidence (rev 65). Cap stays at 5,000 rows even with the filter. Pass?format=jsonfor the same rows as a typed JSON payload (rev 122 β closes the rev-121 follow-up at the dashboard surface). - Outputs CSV / JSON export β
/api/workspace/artifacts-exportreturns the workspace artifact catalog (kind, status, title, summary, tags, share URL, view count) as CSV (default) or JSON (?format=json) β pairs with the JSON export for procurement / SOC 2 evidence (rev 22; rev 122 added JSON variant). - Decisions CSV / JSON export β
/api/workspace/decisions-exportreturns just the workspace's decisions log (status β {approved, archived}, kind β brief) as CSV or JSON. Closes the procurement evidence quartet alongside JSON full + activity CSV + outputs CSV (rev 47; rev 122 added JSON variant). - Stale-tasks CSV / JSON export β
/api/workspace/stale-tasks-export?thresholdDays=Nreturns queued/in_progress tasks aged pastthresholdDays(1-60, default 5) with rev-51 cost columns inline as CSV or JSON (?format=json) (rev 50; rev 122 added JSON variant). - Cost summary CSV / JSON export β
/api/workspace/cost-exportreturns a single procurement-friendly CSV (default) carrying trailing-30-day daily AI spend, top-cost tasks, cost-by-source, and cost-by-assignee in workspace timezone. Pass?format=jsonfor the same data as a structured object grouped by axis (rev 60; rev 122 added JSON variant). - Sources CSV / JSON export β
/api/workspace/sources-exportreturns the input set with rev-16 health columns + rev-26 keyword filters + rev-12 7-day signal-rate column. JSON variant projects keyword lists as arrays (rev 96; rev 122 added JSON variant). - Memory CSV / JSON export β
/api/workspace/memory-exportreturns the workspace's durable knowledge (facts / decisions / preferences / lessons) ranked by pinned + importance + recency, with tags inline. JSON variant projects tags as a string[] so analytics tooling doesn't have to re-parse. Closes the procurement-evidence cluster's seventh axis on the durable-knowledge surface alongside the v1 mirror at/api/v1/workspace/memory-export(rev 125 β closes the named rev-124 next-sprint candidate). - Memory TTL by plan β Free 7d, Pro 90d, Team unlimited; high-importance entries (β₯9) and pinned entries are preserved across pruning.
- Public share links β opt-in per artifact, robots-noindexed, revocable in one click. View counts are stored on the artifact and exposed only inside the workspace. External readers can navigate other shared briefs in the same workspace via tag drill-down (rev 45), narrow by artifact kind (rev 46), and narrow by status β approved / ready / archived (rev 47).
Inbound webhook hardening
- All inbound endpoints (
/api/webhooks/signals/{token},/api/webhooks/email/{token}) authenticate via a per-workspace ingest token. The token is rotatable from the dashboard and immediately invalidates prior access on rotation. - Optional per-workspace HMAC signing secret requires every inbound POST to include an
X-Loop-Signature: sha256=<hex>header containing the SHA-256 HMAC of the raw body. Rotate or disable from the integrations panel; covers both webhook surfaces with one secret.
Vendor / model exposure
- OpenAI is the only model provider as of v2.x; tokens are not used for training (per OpenAI API terms).
- Per-cycle input + output token counts are stored on each
desk_runrow and surfaced in-app for cost transparency. - No other AI vendor receives workspace data. There is no shared-tenant inference cache; every request is workspace-scoped.
Mapped to ISO 42001 control families (high level)
- Human oversight β approval boundary + reviewer note + regenerate flow.
- Transparency β activity log + per-task work log + cost transparency widget.
- Risk management β plan-tier memory TTL, signed webhooks, ingest-token rotation, source pause/error states.
- Data governance β workspace JSON export, activity CSV export, share-link revoke, scrubbed exports.
Loop Desk is not yet ISO 42001 certified, but is architecturally aligned with the standardβs controls β meaning customers in regulated industries can deploy it inside an AIMS without building bespoke audit infrastructure on top.
Plans
- Free - 1 workspace, 3 sources, 50 loop cycles/month, 7-day memory
- Pro ($29/mo) - 5 workspaces, unlimited sources, unlimited cycles, 90-day memory
- Team ($79/mo) - 25 workspaces, unlimited everything, team sharing, dedicated support
FAQ
What happens to my data?
Signals are processed and stored in your workspace. We do not share, sell, or use your data for training. You can export or delete at any time.
Can I use Loop Desk for personal tasks?
Loop Desk is designed for business signal processing, not personal to-do lists. For personal productivity, a task manager is a better fit.
How is this different from an email client?
Email is one signal source among many. Loop Desk takes email plus feeds, forms, notes, and webhooks, then clusters, summarizes, and prioritizes across all of them. Email clients show you messages. Loop Desk shows you what matters.
Does Loop Desk send emails on my behalf?
Loop Desk can draft responses, but nothing sends until you approve and explicitly trigger it. The approval boundary is fundamental.