This guide explains how to add new functionality end-to-end in the graph workflow architecture.
It covers two scenarios:
- Add a new activity type (for existing node types like
activityandpollUntil). - Add a brand-new node type (new execution semantics in the DAG engine).
References:
docs-md/graph-workflows/DAG_WORKFLOW_ENGINE.mddocs-md/graph-workflows/GRAPH_TYPES.md
At runtime, a workflow graph follows this path:
- A workflow config is created/updated via backend workflow APIs.
- Backend validates graph schema and activity type strings.
- Config is stored in
workflows.config(JSONB). - OCR request loads the selected workflow config and starts Temporal
graphWorkflow. - Temporal workflow validates graph defensively again.
- Graph runner executes nodes (
activity,switch,map,join,childWorkflow,pollUntil,humanGate). - Activity nodes resolve
activityTypestrings to real activity functions via the worker registry.
Main files in this flow:
- Shared type definitions:
packages/graph-workflow/src/types.ts(package@ai-di/graph-workflow) - Shared graph validator:
packages/graph-workflow/src/validator/validator.ts - Backend save-time validation:
apps/backend-services/src/workflow/graph-schema-validator.ts - Backend activity allow-list:
apps/backend-services/src/workflow/activity-registry.ts - Workflow start:
apps/backend-services/src/ocr/ocr.service.ts - Temporal client start call:
apps/backend-services/src/temporal/temporal-client.service.ts - Worker activity registration:
apps/temporal/src/activity-registry.ts - Worker workflow-safe activity type list:
apps/temporal/src/activity-types.ts - Temporal workflow entrypoint:
apps/temporal/src/graph-workflow.ts - Graph execution loop:
apps/temporal/src/graph-engine/graph-runner.ts - Node execution handlers:
apps/temporal/src/graph-engine/node-executors.ts
Use this when you do not need a new node type. You only want a new action that runs inside an existing activity node (or pollUntil node).
Example: add document.normalizeMetadata.
Create a new file:
apps/temporal/src/activities/<your-activity-file>.ts
Export the function with a strongly typed input/output contract.
Then export it from:
apps/temporal/src/activities.ts
Modify:
apps/temporal/src/activity-registry.ts
Add a register({...}) entry with:
activityTypestring (used in graph JSON)activityFnreference- metadata (
defaultTimeout,defaultRetry,description)
Note: execution currently uses node-level timeout/retry defaults in node-executors.ts (2m and maximumAttempts: 3) when not specified on the node.
Modify:
apps/temporal/src/activity-types.ts
Add your activityType string to REGISTERED_ACTIVITY_TYPES.
This matters because workflow code cannot import worker-only modules directly; it validates with this constants file.
Modify:
apps/backend-services/src/workflow/activity-registry.ts
Add the same activityType string and description.
Without this, backend graph validation rejects configs using your new activity.
Update/create tests:
apps/temporal/src/activities/<your-activity-file>.test.ts(new)apps/temporal/src/activity-registry.test.ts(update expected activity list)apps/backend-services/src/workflow/activity-registry.spec.ts(update expected activity list/count)
Also add/adjust graph validation tests if needed:
apps/backend-services/src/workflow/graph-schema-validator.spec.tsapps/temporal/src/graph-schema-validator.test.ts
Use in workflow JSON as:
- node
type: "activity"ortype: "pollUntil" - set
activityType: "<your.activityType>" - map
inputs/outputsports toctxkeys
Useful places for examples/templates:
docs-md/templates/*.json(if maintaining templates)- Workflow configs stored via API/UI
Use this when your new behavior cannot be expressed with existing node types and requires new graph semantics.
Example: a hypothetical batch node.
Modify packages/graph-workflow/src/types.ts (the single source of truth for all graph types):
- Add the new type to the
NodeTypeunion - Define the new node interface extending
GraphNodeBase - Add it to the
GraphNodediscriminated union
The three app-level type files (apps/backend-services/src/workflow/graph-workflow-types.ts, apps/temporal/src/graph-workflow-types.ts, apps/frontend/src/types/graph-workflow.ts) are all thin re-exports (export * from "@ai-di/graph-workflow") and automatically pick up the changes.
Modify:
apps/temporal/src/graph-engine/node-executors.ts
Add a new case in executeNode(...) and implement handler logic.
If your node affects dependency readiness/routing semantics, also review:
apps/temporal/src/graph-engine/graph-algorithms.tsapps/temporal/src/graph-engine/graph-runner.ts
Modify the shared validator in packages/graph-workflow/src/validator/validator.ts. Both backend and temporal use thin wrappers around this shared function (apps/backend-services/src/workflow/graph-schema-validator.ts and apps/temporal/src/graph-schema-validator.ts), so changes here apply to both validation contexts automatically.
Add validation for:
- required fields of new node
- cross-node references
- edge constraints
- deterministic safety constraints if relevant
Modify:
apps/frontend/src/components/workflow/GraphVisualization.tsx
At minimum, update node type maps:
- dimensions
- colors
- icons
- render logic for any special shape/behavior
If the editor performs type checks or assumptions, also update:
apps/frontend/src/pages/WorkflowEditorPage.tsx
Update tests in all layers:
- Temporal node execution tests (graph workflow and/or graph-engine tests)
- Backend validator tests
- Frontend visualization tests (if present for type rendering)
Core files to review:
apps/temporal/src/graph-workflow.test.tsapps/temporal/src/graph-engine/graph-algorithms.test.tsapps/temporal/src/graph-schema-validator.test.tsapps/backend-services/src/workflow/graph-schema-validator.spec.ts
For activity and pollUntil nodes:
- Input object is built from:
- resolved
inputsport bindings fromctx - merged
parametersobject
- resolved
- Activity return value is expected to be an object.
- For each output binding, executor reads
result[binding.port]and writes toctx.
So the activity return object keys must match output port names exactly.
Relevant implementation:
apps/temporal/src/graph-engine/node-executors.tsapps/temporal/src/graph-engine/context-utils.ts
- Implement activity in
apps/temporal/src/activities/ - Export from
apps/temporal/src/activities.ts - Register in
apps/temporal/src/activity-registry.ts - Add string to
apps/temporal/src/activity-types.ts - Add string to
apps/backend-services/src/workflow/activity-registry.ts - Update temporal registry tests
- Update backend registry tests
- Add/update validator tests where relevant
- Add/update docs-md/template graph examples
- Add node type/interface in
packages/graph-workflow/src/types.ts(all three apps pick it up automatically via re-exports) - Implement executor behavior in
apps/temporal/src/graph-engine/node-executors.ts - Extend the shared validator in
packages/graph-workflow/src/validator/validator.ts - Update frontend graph visualization for new node type (
apps/frontend/src/components/workflow/GraphVisualization.tsx) - Add node-type-specific tests in temporal/backend/frontend
- Update docs (
docs-md/graph-workflows/GRAPH_TYPES.mdand this guide)
-
Activity type strings must stay synchronized across:
- backend
activity-registry.ts - temporal
activity-types.ts - temporal
activity-registry.ts
- backend
-
Backend may accept/reject at save time differently than runtime if those lists drift.
-
getWorkflowGraphConfigis an internal Temporal activity used by child workflow behavior. Do not rely on it as a user-facing graph node unless explicitly intended. -
Node-level timeout/retry values in graph config drive execution behavior. Registry defaults are metadata for discoverability/tests unless explicitly wired into execution.
-
New node types must preserve Temporal determinism and avoid non-deterministic workflow-side behavior.
- Implement temporal activity or node executor logic.
- Update temporal type constants/unions.
- Update backend validation/type definitions.
- Update frontend rendering/types.
- Add tests in temporal + backend (+ frontend for new node type).
- Update docs and workflow templates.
This order catches most contract mismatches early and keeps schema/type changes aligned with execution behavior.
Three OCR correction tools are registered as activity types and can be used in graph workflows:
Dictionary-based spellcheck on OCR field values. Uses nspell with an English dictionary.
Parameters:
language(string, optional, default:"en") — language codefieldScope(string[], optional) — restrict to specific field keys
Input binding: ocrResult (from ctx.ocrResult or ctx.cleanedResult)
Output: { ocrResult, changes, metadata } (CorrectionResult)
Character confusion map replacements (O→0, l→1, S→5, etc.) with optional custom map override.
Parameters:
confusionMapOverride(Record<string, string>, optional) — overrides default confusion mapapplyToAllFields(boolean, optional, default: false) — apply to all fields, not just date/number-likefieldScope(string[], optional) — restrict to specific field keys
Input binding: ocrResult (from ctx.ocrResult or ctx.cleanedResult)
Output: { ocrResult, changes, metadata } (CorrectionResult)
Deterministic field normalization: whitespace cleanup, digit grouping, date separators.
Parameters:
documentType(string, optional) — LabelingProject id for schema-aware rulesenabledRules/disabledRules(string[], optional)normalizeFullResult(boolean, optional)normalizeWhitespace(boolean, optional, default: true)normalizeDigitGrouping(boolean, optional, default: true)normalizeDateSeparators(boolean, optional, default: true)fieldScope(string[], optional) — restrict to specific field keysemptyValueCoercion(none|blank|null, optional, default:none) — after rules, coerce empty fields to""or JSON null for benchmark GT alignment (all fields in the OCR payload; not filtered byfieldScope)
Input binding: ocrResult (from ctx.ocrResult or ctx.cleanedResult)
Output: { ocrResult, changes, metadata } (CorrectionResult)
{
"correctionNode": {
"id": "correctionNode",
"type": "activity",
"label": "Spellcheck Correction",
"activityType": "ocr.spellcheck",
"parameters": { "language": "en" },
"inputs": [{ "port": "ocrResult", "ctxKey": "cleanedResult" }],
"outputs": [{ "port": "ocrResult", "ctxKey": "cleanedResult" }]
}
}A programmatic manifest of the three AI-recommendable correction tools and their parameters is available via apps/temporal/src/correction-tool-registry.ts. The AI recommendation pipeline (Feature 008 Step 3) uses it for tool IDs and parameter schemas; node placement for candidate workflows is fixed: split the first normal edge after structured OCR (azureOcr.extract, mistralOcr.process, … — OCR_CORRECTION_AFTER_ACTIVITY_TYPES in @ai-di/graph-insertion-slots; see docs-md/OCR_IMPROVEMENT_PIPELINE.md), not per-entry “safe insertion” metadata in the registry.
See also: docs-md/OCR_CONFUSION_MATRICES.md for confusion matrix documentation.