Architecture Deep Dive
Applies to: ChengOS v0.1.0+ | Last verified: 2026-08-12 | Source:
crates/cheng-core/src/ports/mod.rs,crates/cheng-core/src/ports/executor.rs,crates/cheng-engine/src/executor,chengflow/CLAUDE.md
ChengOS's backend is a hexagonal architecture (ports and adapters) with domain-driven
design. That is not a slogan on this project — it is enforced by the crate graph: cheng-core
has no database, no HTTP, and no filesystem dependencies, so a domain rule cannot quietly reach
for a connection pool.
The layers
┌─────────────────────────────────────────────┐
│ Domain (cheng-core) │
│ ┌─────────────────────────────────────┐ │
│ │ Business logic & domain models │ │
│ └──────────────┬──────────────────────┘ │
│ │ uses │
│ ┌──────────────▼──────────────────────┐ │
│ │ Ports (traits) │ │
│ │ WorkflowRepository, ExecutionRepo, │ │
│ │ Executor, EventPublisher, … │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
▲
│ implements
┌─────────────────┴───────────────────────────┐
│ Adapters (infrastructure) │
│ DieselWorkflowRepository (cheng-storage) │
│ WorkflowEngine (cheng-engine) │
│ EventBusAdapter / WsManager (cheng-api) │
└─────────────────────────────────────────────┘
| Crate | Layer | Owns |
|---|---|---|
cheng-core | Domain | Models, ports, domain services, domain events. No infrastructure. |
cheng-storage | Infrastructure | Diesel repositories, migrations, domain ↔ row conversion |
cheng-engine | Application | WorkflowEngine, DAG analysis, scheduling, lifecycle |
cheng-api | Presentation | Axum REST, WebSocket, dependency injection in main.rs |
Dependency injection happens exactly once, in main.rs. There is no global state, which is
why the engine and every service are testable with in-memory fakes and why the same
Executor can be handed to a REST handler, the CLI, the scheduler, and a
subflow node.
Ports
Ports are traits in cheng-core/src/ports/. The list is long — over forty — and reading their
names is the fastest map of the system: WorkflowRepository, ExecutionRepository,
ConversationRepository, DocumentRepository, DocumentShadowRepository, TableRepository,
KnowledgeBaseRepository, ArtifactStore, ExecutionArchiveStore, TraceSnapshotStore,
ScheduledTaskRepository, CredentialRepository, ChannelRepository, ChannelAdapter,
AdapterRegistry, SkillRepository, McpServerRepository, ApprovalEventBus,
ShadowEventBus, EventPublisher, Executor, NodeRegistry, WorkflowFileImporter,
WorkspaceToolBridge, and more.
The Executor port is the one to understand first. execute(workflow, options) starts a run
and returns immediately with an ExecutionId; the run proceeds in the background.
execute_by_id loads from the repository first, which is what the frontend uses once a workflow
is saved.
Execution pipeline
1. REST receives an execution request
2. WorkflowEngine.execute() with the workflow id
3. Load from WorkflowRepository
4. Build a DAG from nodes and edges (petgraph)
5. Create the Execution record
6. Background task:
topological sort → ready nodes
TaskScheduler spawns async tasks
node.execute(ExecutionContext)
results persisted
EventPublisher → WebSocket
repeat until done or failed
7. Final state update
8. WebSocket broadcasts completion
The engine splits cleanly in two: the static half (DAG planner, validation, topological sort, stage selection) is pure and stateless; the dynamic half (task scheduler) runs at runtime. Because the plan is derived rather than stored, changing a workflow can never leave a stale plan behind. Execution Model covers this in depth.
Key domain types
- Strongly-typed ids:
WorkflowId,NodeId,ExecutionId— UUID newtypes, so you cannot pass an execution id where a workflow id belongs. NodeValue: a tagged union of String, Number, Boolean, Array, Object, File, Stream. TheFileandStreamcases are what let large payloads move by reference and let a model node emit tokens downstream as they arrive.ExecutionState:pending,running,paused,waiting_for_review,completed,failed,cancelled,timeout— eight states, the last four terminal.- Component identifiers: node types are
category/name, e.g.io/input_text,ai/llm.
The node system
Nodes are registered by macro at compile time. #[derive(Node)] supplies metadata;
register_node! / register_async_node! add the type to an inventory the registry collects at
startup. There is no registration list to maintain — adding a file and rebuilding is enough.
Three implementation shapes:
| Trait | Use for |
|---|---|
TypedNode | Synchronous, type-safe pure compute |
AsyncTypedNode | Async, I/O-bound or context-dependent |
AsyncNode | Raw HashMap<String, Value> — legacy |
The engine fetches the definition from NodeRegistry, validates the inputs against the node's
declared spec, calls execute with an ExecutionContext, and stores the outputs for downstream
nodes.
Schema-driven UI
The backend emits JSON Schema with x-* UI extensions for every node; the frontend's Field
Control Registry turns that into form controls by priority matching. The consequence for
development is direct: changing a node's inputs or outputs on the backend surfaces
automatically in the UI, with no frontend change.
The priority chain is ordered — connected port status (100), file paths (90), sensitive field
names (85), component identifiers such as model_AC (65), enums (45), basic types (40),
fallback (0). Understanding it explains why a field renders as it does. See
Node Configuration.
Supported extensions include x-control, x-provider-options, x-model-options,
x-depends-on, x-allow-custom, x-hidden, and x-port.
Cross-cutting patterns
- Event-driven —
EventBusAdapterbridges engine events to WebSocket. - Async everywhere — Tokio; all I/O is
async. - Streaming — nodes can yield incremental results through the stream publisher.
- Errors —
anyhow::Resultfor application errors,thiserrorfor domain errors. - Credentials —
EncryptionServiceplusCredentialRepository; an OAuth2 background service refreshes tokens. - Channel auth —
chk_-prefixed API keys stored as credentials withprovider = "channel_adapter", checked by middleware. - Ambient context —
workspace_id,conversation_id,document_idinjected by the engine, force-overriding model-supplied values in tool calls.
The frontend architecture
chengflow-ui is React 18 + TypeScript + Vite, with TanStack Router (file-based routes),
Zustand + Immer for client state, React Query for server state, React Flow for the canvas, and
Tailwind + Radix + shadcn/ui for presentation.
State is split across purpose-built stores rather than one global blob: editorStore (node and
edge CRUD, viewport, clipboard, validation), workflowTabStore (tabs and backend sync),
historyStore (undo/redo), executionStore (WebSocket execution state), workspaceStore
(active workspace), shadowStore (reviewable AI edits).
The app is workspace-scoped. Routes come in two shapes — legacy global (/chat,
/editor/$id) and workspace-scoped (/w/$workspaceId/...). workspaceStore is authoritative
for activeWorkspaceId, and feature modules read it rather than touching localStorage.
Resolution order on boot: URL → localStorage cache → backend /me/preferences → first
accessible workspace. The store never auto-creates a workspace.
Separately from the editor, src/runtime/ renders PageLayout JSON — the output of workflows —
into live pages: a miniature app-rendering engine, with scripts/generate-pages.ts as its
build-time counterpart.
Cloud service invariants
chenghub and skill-registry live outside this tree and hold to their own rules: PostgreSQL
is the only source of business truth and Redis is an accelerator that both services degrade
gracefully without; each has a single binary whose migrate subcommand is the only
schema-mutating path; the skill registry never receives ChengFlow credentials, never executes
skills, and content-addresses artifacts by SHA-256 with published artifacts immutable.
Next steps
- Crate Structure — every crate and its job.
- Writing a Custom Node — applying all of this.
- Execution Model — the engine in detail.