Skip to content
Harshdeep Singh

Case study

Semantask

Real-time work coordination

An open-source work-coordination platform. Teams talk in chat, conversation becomes reviewable suggestions, managers approve, and tool-running autonomy stays optional and policy-gated.

  1. 01Conversation
  2. 02Suggestion
  3. 03Approval
  4. 04Execution

Execution is optional. Tools run only when org policy allows; the default mode is suggest_only.

Status
Open source, MIT
Demo
None hosted — screenshots below
Shape
Turborepo: web, socket, task-worker
Stack
Node.js 24, TypeScript, MongoDB, Redis

01Product

What it looks like

Semantask conversation view: chat messages with AI-detected work cards showing confidence, and Dismiss or Review & approve buttons; extracted tasks listed on the right.
Chat stays the source of truth. Extracted work shows up inline as suggestions with a confidence score, to dismiss or review and approve.
Semantask suggestions inbox: suggestions waiting for review, each with an assignee picker, Accept & assign, and a Dismiss action that requires a reason.
The suggestions inbox. Accepting creates a task and does not run tools; dismissing requires a reason.

Local build with seeded demo data — the names and messages are not real users.

02Problem

Why this is hard

Chat is where work gets agreed. Turning that conversation into tasks automatically is only useful if tools never fire on their own: in Semantask's suggest-first contract (ADR-005), a tool side effect nobody approved is a P0 bug.

Underneath, that becomes a systems problem. Chat has to stay real-time, extraction and execution have to run off the request path, and a crashed or duplicated worker must never repeat a side effect such as an email or a GitHub issue.

03Approach

The shape of the solution

  1. 01

    Suggest-first: conversation becomes reviewable suggestions, approvals stay with humans, and organization policy decides whether tools may run at all.

  2. 02

    Ingress classification calls an LLM by default and falls back to the regex classifier when the model is missing or fails; a confidence threshold decides what becomes a suggestion. Assignee and due-date candidates stay heuristic.

  3. 03

    The chat stack it grew from stays: HTTP persists first, Socket.IO only transports, Redis coordinates rooms and ephemeral state.

  4. 04

    Execution moves to a dedicated task worker fed by a transactional outbox, with DB leases, heartbeats, and idempotency at the event, action and tool layers.

04System

How the pieces fit

Semantask system overview: the client writes over REST to the Next.js API, which persists to MongoDB and enqueues an outbox event in the same transaction. A transport-only Socket.IO server authorizes over an internal HTTP bridge and uses Redis for the adapter and presence. The task worker claims outbox events under a lease and calls LLM providers and tools when policy allows.
apps/web
Next.js UI, APIs, auth, onboarding and the /app workspace. The only writer.
apps/socket
Transport-only Socket.IO server. Schema-free.
apps/task-worker
Outbox consumer; runs the AgentRunner and tools only when policy allows.
apps/mobile
Optional Expo client (untested).
packages/*
auth, db, observability, services, types — five coarse shared packages.

05Engineering decisions

What was chosen, and what it cost

  1. D1

    Socket server is transport-only

    apps/socket authenticates, authorizes, and fans out events. It never writes a Message to MongoDB; the Next.js API is the only writer and the only outbox producer.

    Rejected
    Persisting in the socket handler (stateful, schema-coupled) and dual-write (two writers, guaranteed drift).
    Cost
    Persist-then-broadcast ordering is a client contract, not a server guarantee. Clients reconcile the live event against the persisted REST view.
  2. D2

    Authorization over an internal HTTP bridge (ADR-003)

    The socket server makes trust decisions by calling private web endpoints guarded by a shared internal secret. Identity is re-validated against Mongo on connect, never from claims alone.

    Rejected
    Giving the socket server Mongoose models (re-couples the schema) and trusting JWT claims (a banned user keeps operating until token expiry).
    Cost
    A network hop per authorized operation, a dependency on web uptime, one unscoped shared secret, and a 45s revocation lag.
  3. D4

    Transactional outbox as the integration boundary (ADR-002)

    The domain change and an OutboxEvent commit in one Mongo transaction; the worker claims events with an atomic findOneAndUpdate.

    Rejected
    Direct RPC from web to worker (a worker outage drops work) and a dedicated queue (more operational surface).
    Cost
    At-least-once delivery, and the retry scanner requires a replica-set Mongo.
  4. D5

    Layered idempotency instead of chasing exactly-once

    Accept at-least-once and enforce idempotency at the event, action, and tool layers. The email tool forwards its key as the provider's Idempotency-Key header so the provider dedupes too.

    Cost
    The original tool key included runId, so a lease steal could re-execute a side effect. The shipped key is run-independent: SHA-256(taskId | stepId | toolName | params).
  5. D6

    Distributed lease and heartbeat for one writer per task

    A DB-held lease on the Task document, acquired atomically and renewed every leaseMs/3. A failed heartbeat aborts the in-flight LLM call and tool fetch.

    Rejected
    Optimistic concurrency alone (does not reliably guard between fetch and save) and a Redis-only lock (lease state would live apart from task state).
    Cost
    Heartbeat tuning, steal windows, and replica-set assumptions for transactional paths.
  6. D7

    Execution state machine is authoritative (ADR-001)

    Fine-grained executionState is the enforcement boundary. Legacy lifecycle fields are projected from it at write time; divergence is logged, not auto-repaired.

    Cost
    Two fields still exist on the document.

06Tradeoffs

Known limits, written down

  • Persist-then-broadcast is a client contract: a misbehaving client can emit without persisting, and receivers see a ghost message until they refresh.

  • Authorization costs an HTTP hop per privileged socket operation and depends on the web app being up.

  • Transactional paths and the outbox retry scanner need a replica-set MongoDB.

  • Delivery is at-least-once; correctness rests on idempotency at the event, action and tool layers.

  • The dev-mode in-memory Redis shim is not horizontally safe.

  • These are open-source projects, not services operated with production user traffic. Scaling notes describe the intended shape, not measured load.

07Technology

What it runs on

Runtime
  • Node.js 24
  • TypeScript
  • pnpm
  • Turborepo
Web
  • Next.js 15 (App Router)
Real-time
  • Socket.IO
  • Redis adapter
Data
  • MongoDB
  • Redis
AI
  • OpenAI
  • OpenAI-compatible
  • Hugging Face
Delivery
  • Docker Compose
  • nginx
  • Playwright e2e
  • GitHub Actions

08How it grew

One repository, three stages

  1. Stage 1

    ChatApp

    Real-time messaging foundation

    Added: The real-time layer: HTTP persists first, Socket.IO only transports, Redis carries the adapter and presence.

  2. Stage 2

    Agent Mesh AI

    Worker / orchestration

    Added: A task worker, a transactional outbox, distributed leases and an LLM provider abstraction.

  3. Stage 3

    Semantask

    Current platform

    Added: The suggest-first contract: conversation becomes suggestions, people approve, policy gates tools.

ChatApp and Agent Mesh AI are earlier names for stages of Semantask, not separate products. One repository: github.com/harshlocham/semantask(opens in a new tab)

09Proof

Read the code

Recent public activity

  • Organization coordination dashboard
  • Playwright end-to-end proof harness
  • Onboarding wizard and first conversation for new accounts
  • Runtime migrated to Node 24 LTS
  • Design system foundation

From the repository's public history.