Skip to content
AI Workforce OS
ES EN
Create account

AI Workforce OS Architecture

AI Workforce OS separates deciding from doing: a Go backend is the source of truth, a Python runtime only reasons with the model, and a Next.js frontend draws the office from events. This page is for anyone who deploys, extends or audits the system: how the pieces fit and how a request travels end to end.

7 min read

On this page
  1. Components
  2. Backend and runtime boundary
  3. Request flow
  4. Realtime channel
  5. Data model at a glance
  6. Multi-tenancy
  7. Simulation versus real AI
  8. Deployment and edge
  9. Repository layout
Back to top

Components#

All business state lives in the backend; the runtime keeps no state between calls. Postgres, Redis, object storage and the sandbox are optional in development and each has a fallback.

ComponentTechnologyResponsibility
FrontendNext.js, React Three Fiber3D office and dashboard mode. Consumes REST and the WebSocket; every animation derives from real events.
BackendGo, chiREST API /api/v1, WebSocket /ws, orchestrator, approvals, audit, budget, authentication and RBAC.
Agent runtimePython, FastAPI, CrewAIReceives a task and context, calls the model provider and returns a structured result.
PostgresPostgreSQLData for all organizations, with row-level security and numbered SQL migrations. Without DATABASE_URL the backend runs in memory for demos.
RedisRedisEvent pub/sub to the WebSocket hub, locks and rate limits. Without REDIS_URL, events and locks are in-process.
Object storageS3-compatible (RustFS in development)Drive file bytes. Without S3_ENDPOINT and S3_BUCKET they are stored in Postgres.
Sandbox runnerGo, DockerRuns code that an agent asked for and someone approved, in disposable containers with no network. Only with SANDBOX_URL and SANDBOX_TOKEN.
CaddyCaddyProduction proxy: automatic TLS, sends /api/* and /ws to the backend and everything else to the frontend.

Backend and runtime boundary#

The core rule: the runtime never talks to Postgres, Redis or external systems. It receives a task with its context and returns a result, tool requests and usage; it only contacts the model provider. The backend decides permissions, autonomy and budget, and is the only one that executes tools.

The backend calls the runtime over HTTP with JSON at RUNTIME_URL. If RUNTIME_TOKEN is set on both sides, every call carries that secret as a bearer token. The AI engine sits behind an interface of its own (AgentEngine), so CrewAI can be replaced without touching the backend.

Runtime endpointUse
POST /v1/plan, /v1/plan-phases, /v1/plan-phase, /v1/replanBreak a request or a project into tasks with dependencies.
POST /v1/run-taskRun a task; returns output, consults, tool_requests and usage.
POST /v1/consult, /v1/review, /v1/counterargumentAgent-to-agent consultation, quality review and a counterargument for high-risk approvals.
POST /v1/synthesize, /v1/summarizeConsolidated report and summaries.
POST /v1/route, /v1/chat-replyRoute a chat message and generate an agent reply.
POST /v1/estimate, /v1/context-sizes, /v1/provider-testCost estimation, prompt size and testing an organization's key.
GET /healthzStatus and mode: simulation or live.

Tip

The agent runtime has its own page with the full contract, the providers and the data policy.

Request flow#

The user's ask reaches the orchestrator, which asks the runtime for a plan, validates it and creates tasks. Each task is run by an agent that may request tools; sensitive actions wait for human approval, and a report is synthesized at the end.

  1. The user submits text with POST /api/v1/requests (or through chat, which routes it). The backend answers 202 with request_id and emits request.received.
  2. The orchestrator asks the runtime for a plan and validates it before accepting it: duplicate keys, unknown or circular dependencies and maximum depth. It then emits plan.created and task.created.
  3. Tasks run respecting depends_on. Each one changes the agent state (agent.state_changed) and may trigger agent consultations (message.sent) and a quality review (task.reviewed).
  4. Tools requested by the runtime go through the autonomy policy and permissions. If approval is needed, a pending record is created (approval.requested) and the task enters awaiting_approval.
  5. A user with permission decides with POST /api/v1/approvals/{id}/decision. The task resumes or is blocked.
  6. When all tasks finish, the runtime synthesizes the report and the backend emits report.created and request.completed.
  7. In parallel, the cost of every call is recorded and the budget is enforced: budget.warning warns and budget.exceeded pauses execution.
Sequence at a glanceText
POST /api/v1/requests {"text": "..."}      -> 202 {"request_id": "..."}
  orchestrator -> runtime POST /v1/plan          -> tasks + depends_on
  backend validates the plan                     -> plan.created, task.created*
  per task: runtime POST /v1/run-task            -> output, consults, tool_requests, usage
  tool_requests -> policy/autonomy check         -> approval.requested (if needed)
  POST /api/v1/approvals/{id}/decision           -> approval.resolved, task resumes
  runtime POST /v1/synthesize                    -> report.created, request.completed

Realtime channel#

GET /ws opens one WebSocket per organization. The backend publishes each event to Redis (channel aiworkforce:events) and a hub forwards the frame only to clients whose org_id matches; a frame without org_id is dropped. A frame with an audience reaches only those users.

On connect a hello event carries the agent list, followed by domain events. There is no replay, so the client must reload state over REST after reconnecting.

  • The server pings every 30 s and closes if no pong arrives within 60 s.
  • A slow client, with 256 frames queued, is disconnected.
  • With authentication on, the socket closes with code 4401 after 15 minutes (the default access-token lifetime) so the client reconnects with a fresh token.
Event frameJSON
{
  "id": "<uuid>",
  "type": "task.completed",
  "ts": "2026-01-01T12:03:22Z",
  "org_id": "<org id>",
  "agent_id": "sales",
  "payload": { "task": { "id": "...", "status": "done" } }
}
GroupEvent types
Connectionhello, error, metrics.updated, activity.logged
Requestsrequest.received, request.queued, request.dequeued, request.resumed, request.status_changed, request.completed, plan.created, report.created
Taskstask.created, task.started, task.completed, task.failed, task.blocked, task.retrying, task.reviewed, task.steered, task.reassigned
Agents and chatagent.created, agent.state_changed, message.sent, chat.message, chat.typing, route.decided, initiative.raised
Approvalsapproval.requested, approval.progress, approval.reminder, approval.resolved
Costscost.estimated, budget.warning, budget.exceeded, budget.resumed, provider.no_balance
Projectsproject.created, project.launched, project.delta, project.status_changed, project.budget_warning, project.plan_change_*
Rewards and officecoins.earned, shop.purchased, item.equipped, item.unequipped, office.item_placed, office.item_removed, office.layout_updated

Note

The table groups the main events; some modules emit additional types with the same frame shape.

Data model at a glance#

Tables live in Postgres and every business table carries org_id. This is the summary view; the full schema is in the numbered migrations under backend/internal/infrastructure/postgres/migrations/.

EntityMain tablesNotes
Organizations and usersorganizations, users, memberships, refresh_tokens, invitationsRoles per membership: owner, admin, member, viewer.
Agentsagents, agent_profiles, agent_settings, agent_controlsState, tools, permissions and autonomy level.
Requests and tasksrequests, tasks, request_runs, task_checkpoints, reportsTask states: pending, running, blocked, awaiting_approval, done, failed.
Approvalsapprovals, standing_approvals, approval_executionsDouble approval, per-project standing permissions and single execution of what was approved.
Projectsprojects, project_templates, plan_changesPlans with nodes, budget and template versions.
Memorymemories, memory_docsLong-term memory per agent.
Skillsskills, skill_versions, skill_assignmentsVersioned, with a review workflow.
Rewardscoin_wallets, coin_ledger, shop_inventory, shop_equipment, shop_officeCoins for finished work and character and office items.
Governance and costaudit_logs, audit_chain_heads, usage_entries, budget_capsChained audit trail, usage ledger and caps.

Multi-tenancy#

The org_id travels inside the access token and is never taken from client input. The HTTP layer puts it in the request context and every query is filtered by it. A user can belong to several organizations and switches between them by opening a new session.

As defense in depth, Postgres enforces row-level security:

  • Each transaction sets app.org_id with set_config(..., true), local to the transaction; the policies compare org_id with app_current_org().
  • If the value is missing, queries see and write no rows: they fail closed.
  • The application runs SET ROLE to a non-owner role (DB_APP_ROLE, default app_user, without BYPASSRLS); migrations use MIGRATE_DATABASE_URL.
  • The WebSocket hub also filters by organization.

Tip

The authentication and multi-tenancy page covers roles, sessions and the RLS policies in detail.

Simulation versus real AI#

The runtime's HTTP contract is the same in both modes; the engine changes. In simulation (with SIMULATION=true or with no provider configured) the runtime returns scripted results with simulated latencies, with no network and no cost. In live mode it uses the configured providers and its GET /healthz reports mode: live and the provider.

If a provider is configured but the live engine cannot start, the runtime answers in simulation with status: degraded and a warning; there is no silent fallback. The backend reflects the mode in its own GET /healthz as runtime_mode.

Backend statusBash
curl http://localhost:8080/healthz
# {"status":"ok","runtime":"up","runtime_mode":"simulation"}

Tip

SIMULATION=true always wins: no network calls are made even when keys are present.

Deployment and edge#

There are three Compose files:

  • docker-compose.yml: development with accounts. Starts postgres, redis, objects (S3 storage), agent-runtime, backend and frontend; the sandbox runner is in the sandbox profile.
  • docker-compose.demo.yml: layered on the previous one for the open demo mode (AUTH_ENABLED=false).
  • docker-compose.prod.yml: production. caddy, postgres, redis, agent-runtime, backend and frontend; only Caddy publishes ports (80 and 443).

Note

In production Caddy terminates TLS, adds security headers (HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy) and routes by path: /api/* and /ws to the backend, everything else to the frontend.

Repository layout#

The backend is layered: api depends on application, which depends on domain; infrastructure implements the ports.

Text
backend/            Go (chi)
  cmd/server/       main.go: composition root (wires every service)
  cmd/sandbox-runner/  code-execution runner
  internal/         domain/ application/ api/ auth/ events/ projects/ drive/
                    skills/ memory/ rewards/ connections/ controls/ audit/ ...
  internal/infrastructure/{postgres,redis,runtime,memory}
  internal/infrastructure/postgres/migrations/   numbered SQL migrations
agent-runtime/      Python FastAPI (CrewAI engine, simulation engine)
  app/              main.py, engine.py, simulation.py, crewai_engine.py, providers.py ...
frontend/           Next.js + React Three Fiber (src/app, src/components, src/lib)
e2e/                Playwright
sandbox/            sandbox container images
Caddyfile           production edge (TLS, reverse proxy)
docker-compose.yml  dev stack; docker-compose.demo.yml, docker-compose.prod.yml
docs/               SPEC.md and architecture/*.md
landing/            this public site