Skip to main content

How a front-end workflow executes on the back end

This page stops talking about front-end/back-end integration in general terms and instead tears apart the full pipeline of a single workflow from edit state to execution state.

If you want to know:

  • what happens after document.toJSON()
  • which canvas elements never reach the back end
  • how report and trace get back to the front end

then this is the page to read.

1. Start with the full execution pipeline

The main path in the current reference implementation is:

Flowgram.ai editor
-> document.toJSON()
-> normalizeWorkflowForBackend(...)
-> serializeWorkflowForBackend(...)
-> POST /tasks/validate
-> POST /tasks/run
-> FlowGramTaskController
-> FlowGramRuntimeFacade
-> FlowGramRuntimeService
-> node execution
-> report/result/trace projection
-> front-end runtime snapshot

The real key is not any single interface, but rather when the edit-state object gets compressed into an execution-state object.

2. Stage 1: the edit-state workflow is exported to JSON from the canvas first

Before submitting a task, the front-end runtime exports the current workflow JSON from WorkflowDocument.

The object obtained at this stage still carries an editor-perspective structure.

That means it may contain:

  • pure UI nodes
  • block boundaries that only matter to the editor
  • front-end internal type names

If this were sent straight to the back end, the back-end contract would be polluted by front-end internal structure.

3. Stage 2: normalizeWorkflowForBackend(...) performs execution-state normalization

The real bridge point lives in:

  • ai4j-flowgram-webapp-demo/src/utils/backend-workflow.ts

3.1 It first filters out UI-only nodes

Today, these are explicitly removed:

  • Comment
  • Group
  • BlockStart
  • BlockEnd

This is a major design decision: canvas structure is not execution structure.

3.2 It then normalizes types

For example:

  • llm -> LLM
  • tool -> TOOL
  • knowledge -> KNOWLEDGE

This step converts the type in the front-end node registry into the type the back-end runtime recognizes.

3.3 It also applies local data fixes

For example, a loop node gets its input schema and inputsValues.loopFor filled in. This shows that what the front-end hands the back end is not just "drop a few fields" — it also does protocol backfill.

3.4 It cleans up invalid edges

An edge only enters the back-end schema when both its source and target nodes exist and are valid.

This saves the back end from carrying the full cost of cleaning up canvas-state dirty data.

4. Stage 3: the runtime plugin organizes the call order

The front-end does not just POST /run the moment you hit run; instead WorkflowRuntimeService orchestrates a standard chain.

4.1 First, run local form validation

This walks every node form and makes sure the most basic editor constraints pass first.

4.2 Then call /tasks/validate

This escalates errors to the back-end schema perspective, for example:

  • the back end does not support the node type
  • a required binding is missing
  • the root graph has no End

4.3 Once validation passes, call /tasks/run

At this point the back end returns a taskId, not the final output.

4.4 Then poll report at a fixed interval

Current front-end constant:

  • SYNC_TASK_REPORT_INTERVAL = 500

This means the system default is a "polling observation" model, not a "server-side continuously pushes" model.

4.5 After the task ends, fetch result

The final result is not inferred from report; it is officially obtained through result.

5. Stage 4: back-end controller / facade / runtime enter execution in layers

5.1 Controller stage

FlowGramTaskController only catches the REST request and hands it to FlowGramRuntimeFacade.

5.2 Facade stage

FlowGramRuntimeFacade carries the platform-level semantics:

  • caller resolution
  • access check
  • ownership creation
  • task store update
  • trace / node details assembly

5.3 Runtime stage

FlowGramRuntimeService is what actually handles:

  • schema validation
  • TaskRecord creation
  • graph dispatch
  • node status / workflow status update
  • report / result aggregation

So the sentence "how a front-end workflow executes on the back end" really means: it first enters the platform control plane, then enters the execution engine.

6. Stage 5: node inputs are already resolved by the runtime before the executor runs

This is the most easily misunderstood part of custom nodes.

Before calling the executor, executeCustomNode(...) first:

  • reads the node inputsValues
  • resolves REF / CONSTANT / TEMPLATE / EXPRESSION using internal runtime logic
  • applies input schema defaults

Only then does it put the result into:

  • FlowGramNodeExecutionContext.inputs

So the context.inputs a custom executor receives is usually already an "execution-state input", not the raw front-end configuration object.

7. Stage 6: the back-end projects the execution state back into the front-end read side

After the back end finishes executing, it does not throw the internal object back to the front end as-is.

7.1 FlowGramProtocolAdapter owns the base response protocol

It turns the runtime output into:

  • FlowGramTaskReportResponse
  • FlowGramTaskResultResponse

7.2 trace does one more front-end projection

If traceEnabled is on, the facade also aggregates runtime events into a FlowGramTraceView and attaches it to the report / result.

This layer of data is better suited for direct front-end rendering:

  • top-level status
  • node highlighting
  • timeline
  • token / cost metrics

7.3 The front-end runtime then folds it into a snapshot

The final front-end runtime state does not stop at receiving the HTTP response; it further assembles:

  • validation
  • report
  • trace
  • result
  • errors
  • status

into a continuously evolving WorkflowRuntimeSnapshot.

8. The most common breakpoints in this pipeline

8.1 Broken at the normalization stage

Symptom:

  • the front-end canvas looks normal
  • the back-end receives the wrong node type

Usual cause:

  • forgot to add to BACKEND_TYPE_MAP
  • the node was misclassified as a UI-only type

8.2 Broken at the validate stage

Symptom:

  • /tasks/validate returns a structured error

Usual cause:

  • multiple Starts
  • no End
  • a required input was not bound
  • the node type is not registered

8.3 Broken at the report polling stage after run

Symptom:

  • there is a taskId
  • but the front end never sees a terminal state

Usual cause:

  • the executor is stuck
  • an external HTTP / model call timed out
  • the polling logic does not handle the terminated state correctly

8.4 Broken at the read-side mapping stage

Symptom:

  • the back end actually ran
  • but the front-end nodes are not highlighted or show no result

Usual cause:

  • the front end did not consume report.nodes correctly
  • only looked at result, ignored report / trace
  • the output field path does not match what the front-end UI assumes

9. Why this execution pipeline is valuable

It lets the system fall into three natural segments:

  • editor concerns
  • execution concerns
  • read-side rendering concerns

This is easier to debug than "cram everything into one request", and is more amenable to platformization.

If you keep building complex nodes or front-end panels, the next reads are:

  1. Front-end custom node development
  2. FlowGram custom node extension