Flows and actors
Flows and actors is the part of Wildo that runs AI agents and multi-step work inside an application, and shows that work to the people using it. An application declares an agent (which model answers, what it is for, which records it may read or change), groups agents into a system, and the engine does the rest: it talks to the model vendor, streams the answer into a chat screen, persists the conversation as an ordinary record, pauses when a person must approve an action, and opens the same agent to outside agents over standard protocols. The leverage is that the agent reaches a record through the same operation, the same role checks and the same tenant isolation as a web request, so giving an application an assistant does not open a second door into its data.
The idea
You declare, once, a FlowsActors_Agent (provider, model, prompt as structure, tools by
reference) and a FlowActorSystem that binds actors to agents. Everything else is derived: the
provider client and its secret, the provider-shaped prompt, the streaming channel, the persisted
conversation in three owner scopes, the approval pause and its resume, the tracing of every model
call, the chat screen, and the A2A agent card that lets another organisation’s agent talk to yours.
A tool is a resource operation the application already has, run as the caller, so authorisation is
never re-implemented for the agent. The same word, execution, also names a second, separate
runtime on the frontend: an execution view is a rich screen that choreographs ordinary resource
operations with frontend-only state, declared on the resource it belongs to. This document covers
both, and says which parts are type-only today.
What you get for free
- Provider client construction, secret resolution and rate limiting per provider, from a
providerRefon the agent. - A provider-shaped system prompt compiled from a structured
promptSpec, with trust-level separation of context. - Token streaming to the browser over the application’s existing WebSocket, with rate limiting per event.
- A persisted transcript per conversation, in the caller’s own scope, with read, list and delete as ordinary operations and the engine’s privacy treatment applied.
- Tool execution as the caller: the operation’s role checks, tenant scope and server-set fields hold without a line of agent-specific authorisation.
- A human-in-the-loop pause that survives the turn, with approve and deny both resuming.
- A streaming chat screen, addressable at
/flows-actors/:ref, from one view declaration. - A trace of every model call, and an analytics sink when an observability provider is selected.
- An A2A agent card and task plane, and MCP tools over exposed operations, from the same declarations.
- On the frontend runtime: participations hydrated through ordinary operations, per-action
status, dirty and error aggregation, and a local pane, from one
useExecution()call.
Where you plug in
flowsActorson a backend module:flowActorSystemsFactoryMap,agents,functionTools.FlowsActors_Agent.promptSpecfor behaviour;capabilities.tools.functionRefsfor reach.resourceReadTool(type),resourceWriteTool(type, op, { inputSchema, requiresHumanApproval }), or a hand-writtenFunctionToolfor anything that is not a record set.executionProfileRef, and the execution profiles under the provider’s Agent Client Protocol transport inwildo.saas.config.ts, for a coding agent.flowsActorsViewson a frontend module:FlowsActorsViewDefinitionwithsystemRefs.frontendConfiguration.executionViewson a resource:resourceExecutionView(Component, ...)withanchor,participationsandsurfaceInOperations.mcp: { exposed, description }on a resource operation, to publish it as a tool.AgentCallTraceSink, to route traces somewhere the engine does not.AgentTraceabilityPolicy.level, to decide how much of each call is recorded.
For engineers
How it is built
One model, two kinds of system
The shared model lives in flows-actors. A system is
FlowActorSystem = ActorSystem | FlowSystem (flows-actors.system.shared.definitions.ts), a
discriminated union on kind with FlowActorSystem_Kind.ACTOR and FLOW. Both leaves extend
FlowActorSystem_BaseSchema (flows-actors.system.shared.schemas.ts): ref, displayName,
description, kind, status (FlowActorSystem_Status: draft, active, inactive, deprecated,
archived), visibility (BACKEND_ONLY or FRONTEND) and a config typed by
FlowsActors_Configuration_Schema (flows-actors.configuration.shared.schemas.ts): access
control with a primary scope and an approval requirement, and resilience settings for circuit
breaker, rate limit and bulkhead.
ActorSystemSchema (actors.shared.schemas.ts) is the leaf that owns an actors list. An
Actor has a roleCategory (Actor_Role_Category: coordinator, task planner, operator,
analyzer, reviewer, human in the loop, flow execution), an executionMode
(Actor_ExecutionMode: AGENTIC, FLOW_EXECUTION, HUMAN_INTERACTION) and an agentRef or a
flowRef.
FlowSystem (flows.shared.schemas.ts) owns a graph: triggers (Flow_Trigger_Type: manual,
schedule, webhook, resource event, event, task completion), steps (Flow_Step_Kind: an agent
call, a resource operation, a sub-flow, branch, switch, loop, parallel, delay, retry, memory read
and write, event emit and wait, error catch, compensate and finally, a human-in-the-loop step and
an actor-system call), edges and finishes. It is a hand-written TypeScript type: Flow_Trigger
and Flow_Step have no runtime schema yet, which is why the union is type-only rather than a
z.discriminatedUnion, and why a system is validated with the schema of its kind.
Around the two kinds sit the vocabularies they share: tasks.actors.shared.schemas.ts (the
canonical Actors_Task_Schema with Actor_Task_Topics, actor memory, an execution plan),
flows-actors.io.shared.schemas.ts (FlowsActors_IO_PartKind: text, file, raw data, structured,
image; audio and video are declared and reserved) and the execution envelope
FlowActorSystem_ExecutionSchema with FlowsActors_Status.
Drawn from index.ts:
const todoAssistantActorSystem: ActorSystem = {
ref: 'todo-assistant',
displayName: 'Todo Assistant',
description: 'Ask questions about your todos and lists, grounded in your own data.',
kind: FlowActorSystem_Kind.ACTOR,
status: FlowActorSystem_Status.ACTIVE,
visibility: FlowActorSystem_Visibility.FRONTEND,
config: { timeout: 60000, accessControl: [{ primaryScope: ResourcePrimaryScope.APPLICATION,
requiresApproval: false }], riskLevel: ResourceOperationRiskLevel.LOW },
actors: [{ ref: 'assistant', displayName: 'Assistant',
roleCategory: Actor_Role_Category.OPERATOR,
executionMode: Actor_ExecutionMode.AGENTIC, agentRef: 'todo-assistant.chat.agent' }],
};
What an agent is
FlowsActors_Agent (agents.shared.schemas.ts) is the whole authoring surface for a callable AI
unit. Its config is discriminated by modality on operationKey: an LLM arm
(FlowsActors_Agent_LLMConfiguration, text and structured output), an image arm and a coding arm
(FlowsActors_Agent_CodingConfiguration). Its operationDefinition carries the input and output
schemas of the provider operation. Its promptSpec (FlowsActors_Agent_PromptSpec) is a prompt
authored as structure, not prose: role, persona, skillType, constraints (prohibitions,
highest precedence), instructions, hints, an output specification and an uncertainty policy;
the compiler renders it in each provider’s preferred format. capabilities.tools.functionRefs
names the tools the agent may call.
Three things about model and transport selection are worth stating precisely.
- An agent names
providerRefandmodeldirectly. The model catalogue also definesLLM_Model_Applicability(deep judgement, structured authoring, high-volume mechanical, long-context analysis, visual judgement) with a fail-closedresolveModelForApplicability(llm.external-connectors-models.schemas.ts). Today the embeddings and document-extraction services resolve their sibling applicabilities; the agent runtime does not, and the dogfood chat agent pins a concreteLLM_Model. executionProfileRefselects a transport profile declared by the provider. It is optional on the LLM and image arms and required on the coding arm: a coding agent has no default API path, and the profile is where its environment gate and permission ceiling live.- The coding arm is a real agent, declared like any other, reached over the Agent Client Protocol
(JSON-RPC over stdio).
acp-session-runner.backend.utils.tsspawns the agent, negotiatesinitialize, opens a session against a confined working directory and drives one turn at a time (startACPAgentSession, withrunACPCodingSessionas the single-turn wrapper). The permission ceiling is enforced by the runner’s answer to everysession/request_permission, never by trusting the agent’s own session mode. The application authors the policy half inwildo.saas.config.ts: runner host, authentication mode, permitted environments, working directory policy, permission ceiling.
Tools are FunctionTool values (function-tools.backend.definitions.ts). Two factories cover the
common case: resourceReadTool(type) and resourceWriteTool(type, op, { inputSchema, requiresHumanApproval }). Each rebuilds an ExecutionContext from the caller’s verified
authentication and routes through the ordinary services handler, so the operation’s own role
checks apply and server-set fields cannot be spoofed through tool arguments. The default
functionRef is resource.<type>.<op>. Agent facets and catalogs
(agent-facets.backend.definitions.ts, agent-catalogs.backend.definitions.ts) are reusable
prompt fragments and presets an agent can be composed from through AgentFacetsBackendService.
Drawn from index.ts:
resourceWriteTool(TasksManager_ResourceType.TODOS, CoreResourceOperation.UPDATE, {
requiresHumanApproval: true,
inputSchema: z.object({
id: z.string().min(1).describe('Id of the todo to update. Required.'),
title: z.string().min(1).max(200).optional().describe('New title, if asked.'),
status: z.enum(Todos_Status).optional().describe('New status.'),
}),
}),
The backend runtime
flows-actors holds the runtime.
application module ──registers──▶ FlowsActorsRegistryService
(systems factory map, agents, systems by kind · agents by ref, provider, operation
function tools) function tools by ref
│
WebSocket flows-actors:start ─┐ ▼
A2A message/send, stream ─────┼──▶ ConversationalExecutionBackendService
│ owner scope → persisted variant · server-held history
│ chat actor → agentRef · approval pause and resume
│ │
│ ▼
│ AgentsBackendService.invokeAgentStreaming
│ PromptCompilerBackendService (trust levels)
│ provider client + secret · tools as the caller
│ AgentCallTraceSink (usage, latency, outcome)
│ │
└───◀ emitter: started · message deltas · approval request
· status update · completed · error
FlowsActorsRegistryService(flows-actors-registry.service.ts) is initialised with the application’sFlowActorSystem_InitializationFactoryMap, its agents and its function tools, and answersgetSystem(ref, kind),getAgentOrThrow(ref)andgetFunctionTool(ref).AgentsBackendService(agents.backend.service.ts) has two entry points.invokeAgentruns one agent once and awaits the answer; tools are inert on this path, so a one-shot agent must be handed its data.invokeAgentStreamingforwards real token deltas and is the only path that resolves and executesfunctionRefs, in a bounded multi-step loop. Image configurations branch to an image executor; coding configurations open an ACP session.PromptCompilerBackendService(prompt-compiler.backend.service.ts) compiles the agent and the invocation into a provider-shaped prompt, separating context items byPromptIR_TrustLevel(prompt-ir.shared.schemas.ts: high for system data, medium for owned documents and prior artifacts, low for user input and external sources).ContextCurationBackendServiceruns before it and is a basic filter today.ConversationalExecutionBackendService(conversational-execution.backend.service.ts) is the turn runtime: it resolves the owner scope to a persisted variant, loads the server-held transcript (no client-supplied history is trusted), finds the system’s agentic actor, streams the turn through a transport-agnostic emitter, and persists every message.respondToApprovalresumes a paused turn;readExecutionStatusanswers a poll.- Approval is a status, not a side channel. An approval-gated tool moves the execution to
FlowsActors_Status.AWAITING_APPROVAL, the resume state is serialised into the record’spausedApprovalfield (FlowActorExecution_PausedApprovalSchema), and a decision resumes the same execution; a denial resumes it too, so the agent can explain rather than leave a dead thread. Machine principals are refused at the gate, because no named person can answer it. - The WebSocket channel is
CoreWebSocketEvent.FLOWS_ACTORS_START,STARTED,MESSAGE,APPROVAL_REQUEST,APPROVAL_RESPONSE,STATUS_UPDATE,COMPLETEDandERROR(websocket.shared.schemas.ts), handled inwebsocket.service.tswith a per-event rate limit;flows-actors.controller.tsserves/flows-actors/frontend-infoand/flows-actors/historyover HTTP. FlowsBackendService.executeFlowandActorsSystemBackendService.executeActorvalidate the system and its status, then mark the execution completed without executing steps or actor behaviour. They are placeholders; the conversational path above is the runtime that runs.
Executions and tasks as resources
A conversation and a unit of work are ordinary resources, stamped out in three owner scopes by
declarePolymorphicSchemas. flowActorExecutionsPolymorphicDeclaration
(flows-actors-execution.shared.schemas.ts) yields userFlowActorExecutions,
organizationFlowActorExecutions and applicationFlowActorExecutions, each carrying
executionId, systemRef, kind, a status, and a messages[] log with
FlowsActors_ConversationRole. Its configuration
(flows-actors-execution.shared.resources-config.schemas.ts) makes read and list the owner’s
chat-history surface, keeps create and update internal to the runtime, and gives delete a role
floor distinct from read. flowActorTasksPolymorphicDeclaration
(flows-actors-task.shared.schemas.ts, configured in
flows-actors-task.shared.resources-config.schemas.ts) is the addressable, pollable, cancelable
work record, derived from Actors_Task_Schema and linked to its conversation by a soft field, so
deleting a conversation never deletes its work. Both families carry the engine’s privacy
decorations, and both have framework-owned specifications
(resources). The heavy orchestration state
(plan, walkthrough, standaloneTasks) is not persisted today.
The frontend execution runtime
The second runtime is execution. An execution view is a rich
interaction surface over persisted data, with frontend-only runtime state. It is declared on the
resource it belongs to, under frontendConfiguration.executionViews, with the
resourceExecutionView(Component, config) factory (execution-view.schemas.ts): an anchor
(the business subject), participations sourced from the anchor, a parent requirement or a
relationship projection, surfaceInOperations naming the operation hosts it appears in, and
view-local labels. ExecutionViewDefinition is RESOURCE-scoped by construction. The host is
ExecutionView.tsx; the renderer consumes one hook, useExecution(), whose ExecutionValue
exposes the anchor, the participations, actions that run resource operations with per-action
status, loading, dirty (fed by useExecutionDirtyBridge from any embedded form), errors,
embeddedContent and a local pane. The dogfood surface is TodoListFlowExecutionView, a graph
of a todo list, its todos, tasks and links, composed through ordinary operations.
Drawn from todo-lists.ui-behavior.tsx:
executionViews: {
[TODO_LIST_FLOW_EXECUTION_VIEW_REF]: resourceExecutionView(TodoListFlowExecutionView, {
labels: todoListFlowExecutionViewLabels,
surfaceInOperations: [Op.READ],
anchor: { bindingKey: R.TODO_LISTS,
source: ExecutionViewAnchorBindingSource.PARENT_RESOURCE_REQUIREMENT },
participations: {
anchor: { source: ExecutionParticipationSource.ANCHOR },
todoList: { source: ExecutionParticipationSource.PARENT_RESOURCE_REQUIREMENT,
bindingKey: R.TODO_LISTS },
todos: { source: ExecutionParticipationSource.RELATIONSHIP_PROJECTION,
fromParticipationRef: 'todoList', targetResourceType: R.TODOS },
},
}),
},
The agent and chat interface
A frontend module contributes flowsActorsViews: a FlowsActorsViewDefinition
(flows-actors-view.schemas.ts) names one or more
systemRefs, a layoutPreset and whether it is addressable at /flows-actors/:ref. The engine
host FlowsActorsView.tsx renders a streaming chat over the system or systems the definition
names; FlowsActorsContext.tsx subscribes to the WebSocket events and merges streamed deltas into
one message by messageId. A kind-agnostic node and edge projection of the system
(flows-actors.frontend.shared.schemas.ts, extracted by
flows-actors-frontend-extractor.utils.ts) is what lets an actor system and a flow system be
described through one shape, and the graph canvas presets that draw it are used by the low-level
actor and flow-execution slots. The addressable host does not itself compose a graph beside
the chat today.
The screen pieces are the ai-elements preset family
(ai-elements): chat (conversation, message, prompt
input, sources, suggestion), trace (reasoning, chain of thought, plan, checkpoint, inline
citation), decision (approval, decision request), execution (task, tool, queue, confirmation),
graph (canvas, node, edge, controls, panel, toolbar), artifacts (artifact, code block, image, web
preview), meta (agent, context, model selector, open in chat) and the execution stream. Beneath
them the framework’s own low-level slots (flows-actors:
actor, agent config, approval, artifact browser, execution plan, flow execution, flow node, system
catalog, task detail, task memory) each carry a specification under
flows-actors.
Drawn from app-level-views.definitions.module.frontend.ts:
const flowsActorsViewsConfig: FlowsActorsViewDefinition[] = [{
ref: 'todo-assistant-view',
scope: FrontendView_ScopeMode.APPLICATION,
isAddressable: true,
operationLike: CoreResourceOperation.READ,
primaryScope: ResourcePrimaryScope.ORGANIZATIONS,
systemRefs: ['todo-assistant'],
layoutPreset: 'Default',
}];
Deliberations and expert panels
For a judgement call, the framework can convene a panel rather than ask one model once. The
vocabulary is deliberation.shared.schemas.ts: an ExpertWorkDomain an expert is narrow on
(visual brand mark, marketing copy, UI design coherence, brand strategy, information
architecture), a RuntimeExpertProfile, an EvaluationRubric whose criteria travel as data in
the judge prompt, a fixed RubricVerdict keyed by criterion reference, DeliberationRound,
DeliberationSynthesis and a DeliberationRecord with a DeliberationStatus (running,
converged, budget exhausted, failed). Weighted aggregation is deterministic code
(aggregateWeightedScore), and a verdict citing a criterion the rubric does not contain is
rejected. The engine round loop is ActorsSystemBackendService.executeDeliberation(plan), which
returns the record and never writes disk; the build-time companion supplies the experts, the
candidate producers and the persistence (deliberations
and src/expert-personas/, with the committed charter contracts in
expert-personas). Deliberations are engine and companion
machinery, not a surface a generated application exposes to its users today.
Traceability
Every model call produces an AgentCallTrace (agent-traceability.shared.schemas.ts): outcome,
model, token usage, latency, and, at AgentTraceabilityLevel.DEBUG_VERBOSE, the resolved prompt
and raw output. AgentCallTraceKind separates a generation from an embedding, whose vectors never
reach a log or an analytics vendor at any level. Sinks implement AgentCallTraceSink and are
best-effort by contract: a sink failure never fails the call. The engine ships a structured-log
sink (agent-call-trace-sink.backend.ts) and a lazy sink that resolves the provider selected for
EngineCapability.LLM_OBSERVABILITY on first use
(llm-observability-agent-call-trace-sink.backend.ts).
The doors for outside agents
Two protocols expose an application, on two different planes.
- Agent2Agent exposes conversations: the same actor systems, through
a2a.a2a.controller.tsserves the public agent card at/a2a/.well-known/agent-card.jsonand a JSON-RPC endpoint formessage/send,message/stream(token deltas as server-sent events),tasks/get,tasks/cancel,tasks/resubscribeand the push-notification configuration methods; a named agent mounts at/a2a/:instanceRefwith its own card. Work becomes aflowActorTasksrow when the client asks for an immediate return or when an approval gate suspends the turn, and services cover the task lifecycle, cancellation, push delivery and a per-tenant token budget. A dialect codec (a2a-dialect.backend.schemas.ts) lets one handler set answer two protocol revisions. - The Model Context Protocol exposes operations, not agents:
mcpturns every resource operation that declaresmcp: { exposed, description }into a tool, builds its JSON schema, and dispatches a call back through the ordinary services handler. An agent’scapabilities.tools.mcpRefsis declared in the model and consumed by no runtime path today; the tools an agent can call are itsfunctionRefs.
Both doors are driven against the running dogfood stack by the a2a-* and mcp-* scenarios under
e2e, including hostile probes of tenant isolation and approval tampering.
The dogfood application
Wonder Todos declares one agent, one actor system and five tools in
index.ts: two
read tools, a create tool, an approval-gated update tool and a hand-written knowledge-base tool
that calls the application’s own custom operation. They are registered through
moduleBackend_FlowsActorsRegistry (flowActorSystemsFactoryMap, agents, functionTools).
The chat screen is the todo-assistant-view above; the execution view is the todo-list graph. The
engine also ships its own systems, for example the market-generation agent
(market-generation.actors-system.backend.definitions.ts) that an application may override under
the same ref. Wonder CRM declares no actor system.
Boundaries and known limits
FlowSystemis a type without a runtime schema, andFlowsBackendService.executeFlowexecutes no steps. The flow kind is modelled end to end and does not run; the conversational path refuses a flow system with a typed not-implemented error.ActorsSystemBackendService.executeActoris likewise a placeholder; the runtime that runs is the conversational execution of an actor system’s agentic actor.- One-shot
invokeAgentignores an agent’s tools; only the streaming path executes them. - Streaming covers text-producing operations over the API transport; structured, image and coding invocations do not stream.
- Approval pauses are user-delegated only; a machine principal is refused at the gate.
- The applicability-class model selection exists in the catalogue and is used by the embeddings and document-extraction services, not by the agent runtime.
mcpRefson an agent is declarative; no runtime resolves it.- Context curation before prompt compilation is a basic filter, not retrieval reranking.
- Audio and video modalities are declared and reserved; the executor fails closed on them.
- The coding lane runs as a backend subprocess and, in the dogfood profile, only in a development environment; multi-turn continuation on a persistent session is implemented but has not yet been driven against a real agent adapter.
- The persisted execution record carries the transcript and status, not the plan, walkthrough or standalone task state of the orchestration model.
- Execution views are resource-scoped only; their runtime state is frontend-only and is not persisted.
- Deliberations run in the engine and the build-time companion; no generated application exposes a panel to its users.