Skip to content
Pilotoo
v0.1

Server integration

@pilotoo/server mounts the workflow runtime. WorkflowService builds one runtime on the checkpointer port your agents use, with coreNodeKinds plus your kinds, and stores workflows, runs and run events through the workflowStorage port. This page covers the module options, the routes, the visual builder and the agent tools. See Assembling a server for the module itself.

Set the roles that may use the feature, then pass the options you need to the module.

.env
WORKFLOW_ROLES=member,owner
apps/api/src/app.module.ts
import { createQuickJsSandbox } from '@pilotoo/workflow/quickjs';
PilotooModule.forRoot({
workflowKinds: [myKind], // added to coreNodeKinds; one with a core type replaces it
workflowSandbox: createQuickJsSandbox(),
workflowTools: true, // workflow_list, workflow_run (opt-in)
});
Setting Default
WORKFLOW_ROLES (environment) Comma-separated roles that may list kinds, validate, and create and run workflows. Unset means nobody. See Authentication
workflowStorage PostgreSQL (tables created on first use; see Storage) when the module uses it. null without a database: /workflows answers 501 while /workflows/kinds and /workflows/validate still answer
workflowKinds None: coreNodeKinds alone
workflowSandbox None: a code node fails its run with “The host lends no sandbox port”
workflowTools false

The fetch port is the platform’s. The tool, llm and agent kinds are registered too and use the server’s tool registry, model profiles and agent runner.

The local backend keeps workflows in its SQLite file and gives the local owner the workflow role, so the feature is on by default on the desktop. See Local backend.

With workflowTools: true the module registers two tools. They are opt-in: an agent gets them only by naming them in its definition.

tools: [workflow_list, workflow_run]

The user can then ask “run the nightly report” and the agent calls workflow_list to find the id, then workflow_run. Both act as the caller and answer “Not done: …” for a caller without a workflow role or for an unknown workflow. See Tools and Agents.

Tool Input Answer
workflow_list none Each workflow’s id, name, description? and trigger ({ id, type })
workflow_run { workflowId, input?, waitSeconds? } (wait defaults to 60, at most 300) Starts the run and waits. finished: the output as JSON. paused: the run id and each pending interrupt (nodeId, interruptId, payload) for the agent to put to the user. failed: “Not done: the workflow failed: …”. Still running when the wait ends: its run id and that it is still running

@pilotoo/ui/workflows ships a drag-and-drop builder over the WorkflowsApi client from @pilotoo/chat-core. The demo web app uses useWorkflowEditor to load, validate (debounced) and save a workflow, and renders WorkflowBuilder with what it returns.

apps/web/src/features/workflows/workflow-editor-screen.tsx
import { createWorkflowsClient } from '@pilotoo/chat-core';
import { useWorkflowEditor, WorkflowBuilder } from '@pilotoo/ui/workflows';
const api = createWorkflowsClient(apiUrl, fetch, { credentials });
function Editor({ id }: { id: string }) {
const { state, setDefinition, save } = useWorkflowEditor(api, id);
if (state.status !== 'ready') return null;
return (
<>
<button disabled={!state.dirty} onClick={() => void save()}>Save</button>
<WorkflowBuilder
definition={state.definition}
kinds={state.kinds}
issues={state.issues}
ports={state.ports}
onChange={setDefinition}
/>
</>
);
}

Load @pilotoo/ui/tokens.css and @xyflow/react/dist/style.css once. The builder takes the node kinds from GET /workflows/kinds, so your custom kinds appear in its palette with a config form generated from their schema.

All routes require a caller with a workflow role, except the hook.

Route Purpose
GET /workflows/kinds { kinds }: each kind’s type, category, label, description?, configSchema (JSON Schema) and ports. outputs is 'dynamic' when they depend on the config
POST /workflows/validate A definition, with or without id. 200 with { issues, ports }; ports[nodeId] lists the output ports of every node whose kind is known
GET /workflows The caller’s own, oldest first
POST /workflows { name, description?, nodes, edges }; mints the id; 201
GET, PATCH, DELETE /workflows/:id The caller’s own; another owner’s answers 404. PATCH takes any create field. DELETE stops running runs and removes the workflow, its runs, events and checkpoints
POST /workflows/:id/runs { input }; 201 with the run as running. 400 if the workflow does not validate
GET /workflows/:id/runs Runs, newest first
GET /workflows/:id/runs/:runId { id, workflowId, threadId, status, input, output, error, interrupts, startedAt, finishedAt }
GET /workflows/:id/runs/:runId/events Every event the run reported, in order, each with at
POST /workflows/:id/runs/:runId/resume { interruptId, answer }; 200 with the run as running. 400 for a run that is not paused or an interrupt it does not have
POST /workflows/:id/webhook Mint or rotate the webhook token; 201 { webhookToken }
DELETE /workflows/:id/webhook Clear the token; 204
POST /workflows/:id/hooks/:token Anonymous; the token is the credential. 202 { runId }. See Triggers

The request and response shapes are the workflow* schemas in @pilotoo/chat-protocol (Protocol).

Every engine event is kept as a run event and also emitted on the socket as workflow:event with { run, event }, to the room of the run’s thread (thread:<threadId>). run is the run as it is after that event.

A socket joins a run’s room with the command workflow:watch { workflowId, runId }. It is answered { ok: true, threadId }, or { ok: false, error: { code: 'RUN_NOT_FOUND' } } for a run the caller cannot see, whatever the reason.

Workflows and runs belong to their owner (tenant and subject). A caller with a workflow role sees their own only. There is no admin reach.