Output Variables

We primarily categorize output variables into three types:

  1. Output Node Variables: Typically produced by the node and available for subsequent nodes to use.
  2. Output Node Private Variables: Output variables limited to the node's interior (including child nodes) and not accessible by external nodes.
  3. Output Global Variables: Available throughout the entire flow, readable by any node, suitable for storing public states or configurations.
Reading Guide
  • After Variable Introduction, start here to practice how variables are produced.
  • If you work from form configuration, begin with “Method 1: Synchronization via Form Side Effects.” If you need runtime logic or batch updates, skip to the plugin or UI sections.
  • Every example uses ASTFactory; revisit Core Concepts – AST if you need a refresher.

Output Node Variables

Output node variables are bound to the lifecycle of the current node: they are created with the node and removed when the node is deleted. (See Node Scope for details.)

We typically have three ways to output node variables:

How to pick a method
  • Variable definitions tied to form inputs → Method 1.
  • Variables generated at runtime or synchronized in batches → Method 2.
  • Writing variables directly in UI is only for temporary debugging; avoid Method 3 in production.

Method 1: Synchronization via Form Side Effects

Form side effects are usually configured in the node's form-meta.ts file and are the most common way to define node output variables.

When to use it
  • The node’s variable model can be derived from form fields.

provideJsonSchemaOutputs

If the structure of the output variables required by a node matches the JSON Schema structure, you can use the provideJsonSchemaOutputs side effect (Effect) material.

provideJsonSchemaOutputs uses the createEffectFromVariableProvider factory function to create variable providers.

See documentation: provideJsonSchemaOutputs

createEffectFromVariableProvider Custom Output

provideJsonSchemaOutputs only adapts to JsonSchema. If you want to define your own set of Schema, you'll need to customize form side effects.

NOTE

FlowGram provides createEffectFromVariableProvider, which only requires defining a parse function to customize your variable synchronization side effect:

  • parse is called when the form value is initialized and updated
  • The input of parse is the current field's form value
  • The output of parse is variable AST

In the following example, we create output variables for two form fields path.to.value and path.to.value2:

form-meta.ts
import {
  createEffectFromVariableProvider,
  ASTFactory,
  type ASTNodeJSON
} from '@flowgram.ai/fixed-layout-editor';

export function createTypeFromValue(typeValue: string): ASTNodeJSON | undefined {
  switch (typeValue) {
    case 'string':
      return ASTFactory.createString();
    case 'number':
      return ASTFactory.createNumber();
    case 'boolean':
      return ASTFactory.createBoolean();
    case 'integer':
      return ASTFactory.createInteger();
    default:
      return;
  }
}

export const formMeta =  {
  effect: {
    // Create first variable
    // = node.scope.setVar('path.to.value', ASTFactory.createVariableDeclaration(parse(v)))
    'path.to.value': createEffectFromVariableProvider({
      // parse form value to variable
      parse(v: string, { node }) {
        return [{
          meta: {
            title: `Your Output Variable Title`,
          },
          key: `uid_${node.id}`,
          type: createTypeFromValue(v)
        }]
      }
    }),
    // Create second variable
    // = node.scope.setVar('path.to.value2', ASTFactory.createVariableDeclaration(parse(v)))
    'path.to.value2': createEffectFromVariableProvider({
      // parse form value to variable
      parse(v: { name: string; typeValue: string }[], { node }) {
        return {
          meta: {
            title: `Second Output Variable For ${node.form.getValueIn("title")}`,
          },
          key: `uid_${node.id}_2`,
          type: ASTFactory.createObject({
            properties: v.map(_item => ASTFactory.createProperty({
              key: _item.name,
              type: createTypeFromValue(_item.typeValue)
            }))
          })
        }
      }
    }),
  },
  render: () => (
    // ...
  )
}
TIP

If your Schema is complex and you're not sure how to parse it into AST, you can refer to the official material's implementation of JSON Schema conversion to AST: JsonSchemaUtils.schemaToAST

WARNING

When using the VariableSelector official material for variable selection, each output variable defined in the current node will be displayed as an independent tree node, rather than being grouped by node by default.

For more details, please refer to the VariableSelector Material Documentation

Synchronizing Multiple Form Fields to One Variable

If synchronizing multiple fields to one variable, you need to use the namespace field of createEffectFromVariableProvider to synchronize variable data from multiple fields to the same namespace.

form-meta.ts
import {
  createEffectFromVariableProvider,
  ASTFactory,
} from '@flowgram.ai/fixed-layout-editor';

/**
 * Get information from multiple form fields
 */
const variableSyncEffect = createEffectFromVariableProvider({
  // Must be added to ensure side effects from different fields synchronize to the same namespace
  namespace: 'your_namespace',

  // Parse form value to variable
  parse(_, { form, node }) {
    // Note: The form field requires flowgram version > 0.5.5, prior versions can get it through node.form
    return [{
      meta: {
        title: `Title_${form.getValueIn('path.to.value')}_${form.getValueIn('path.to.value2')}`,
      },
      key: `uid_${node.id}`,
      type: ASTFactory.createCustomType({ typeName: "CustomVariableType" })
    }]
  }
})

export const formMeta = {
  effect: {
    'path.to.value': variableSyncEffect,
    'path.to.value2': variableSyncEffect,
  },
  render: () => (
   // ...
  )
}

Using node.scope API in Side Effects

If createEffectFromVariableProvider doesn't meet your needs, you can also directly use the node.scope API in form side effects for more flexible variable operations.

NOTE

node.scope returns a variable scope object for a node, which has several core methods mounted on it:

  • setVar(variable): Set a variable.
  • setVar(namespace, variable): Set a variable under a specified namespace.
  • getVar(): Get all variables.
  • getVar(namespace): Get variables under a specified namespace.
  • clearVar(): Clear all variables.
  • clearVar(namespace): Clear variables under a specified namespace.
form-meta.tsx
import { Effect } from '@flowgram.ai/editor';

export const formMeta = {
  effect: {
    'path.to.value': [{
      event: DataEvent.onValueInitOrChange,
      effect: ((params) => {
        const { context, value } = params;

        context.node.scope.setVar(
          ASTFactory.createVariableDeclaration({
            meta: {
              title: `Title_${value}`,
            },
            key: `uid_${context.node.id}`,
            type: ASTFactory.createString(),
          })
        )

        console.log("View generated variables", context.node.scope.getVar())

      }) as Effect,
    }],
    'path.to.value2': [{
      event: DataEvent.onValueInitOrChange,
      effect: ((params) => {
        const { context, value } = params;

        context.node.scope.setVar(
          'namespace_2',
          ASTFactory.createVariableDeclaration({
            meta: {
              title: `Title_${value}`,
            },
            key: `uid_${context.node.id}_2`,
            type: ASTFactory.createNumber(),
          })
        )

        console.log("View generated variables", context.node.scope.getVar('namespace_2'))

      }) as Effect,
    }],
  },
  render: () => (
    // ...
  )
}

Method 2: Synchronizing Variables via Plugins

In addition to static configuration in forms, we can also freely and dynamically manipulate node variables in plugins through node.scope.

When to use it
  • You need to create or adjust variables across multiple nodes in bulk.
  • You want to auto-populate default variables when the canvas initializes.

Updating via Specified Node's Scope

The following example demonstrates how to obtain the Scope of the start node in the onInit lifecycle of a plugin and perform a series of operations on its variables.

sync-variable-plugin.tsx
import {
  FlowDocument,
  definePluginCreator,
  PluginCreator,
} from '@flowgram.ai/fixed-layout-editor';

export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =
  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({
    onInit(ctx, options) {
      const startNode = ctx.get(FlowDocument).getNode('start_0');
      const startScope =  startNode.scope!

      // Set Variable For Start Scope
      startScope.setVar(
        ASTFactory.createVariableDeclaration({
          meta: {
            title: `Your Output Variable Title`,
          },
          key: `uid`,
          type: ASTFactory.createString(),
        })
      )
    }
  })

Synchronizing Variables in onNodeCreate

The following example demonstrates how to obtain the Scope of a newly created node through onNodeCreate and implement variable synchronization by listening to node.form.onFormValuesChange.

sync-variable-plugin.tsx
import {
  FlowDocument,
  definePluginCreator,
  PluginCreator,
} from '@flowgram.ai/fixed-layout-editor';

export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =
  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({
    onInit(ctx, options) {
      ctx.get(FlowDocument).onNodeCreate(({ node }) => {
        const syncVariable = (title: string) => {
          node.scope?.setVar(
            ASTFactory.createVariableDeclaration({
              key: `uid_${node.id}`,
              meta: {
                title,
                icon: iconVariable,
              },
              type: ASTFactory.createString(),
            })
          );
        };

        if (node.form) {
          // sync variable on init
          syncVariable(node.form.getValueIn('title'));

          // listen to form values change
          node.form?.onFormValuesChange(({ values, name }) => {
            // title field changed
            if (name.match(/^title/)) {
              syncVariable(values[name]);
            }
          });
        }
      });
    }
  })
WARNING

Directly synchronizing variables in UI (Method 3) is a strongly discouraged practice. It breaks the principle of separation of data and rendering, leading to tight coupling between data and rendering, which may cause:

  • Closing the node sidebar prevents variable synchronization, resulting in inconsistency between data and rendering.
  • If the canvas enables performance optimization to only render nodes visible in the view, and the node is not in the view, the联动 logic will fail.

The following example demonstrates how to synchronously update variables in formMeta.render through the useCurrentScope event.

form-meta.ts
import {
  createEffectFromVariableProvider,
  ASTFactory,
} from '@flowgram.ai/fixed-layout-editor';

/**
 * Get information from form
 */
const FormRender = () => {
  /**
   * Get current scope for setting variables later
   */
  const scope = useCurrentScope()

  return <>
    <UserCustomForm
      onValuesChange={(values) => {
        scope.setVar(
          ASTFactory.createVariableDeclaration({
            meta: {
              title: values.title,
            },
            key: `uid`,
            type: ASTFactory.createString(),
          })
        )
      }}
    />
  </>
}

export const formMeta = {
  render: () => <FormRender />
}

Output Node Private Variables

Private variables are variables that can only be accessed within the current node and its child nodes. (See Node Private Scope.)

TIP

Quick rule of thumb: if a variable only serves the node’s internal implementation and shouldn’t be exposed downstream, keep it in node.privateScope.

Here we only list two methods, and other methods can be inferred from Output Node Variables.

Method 1: createEffectFromVariableProvider

createEffectFromVariableProvider provides the parameter scope for specifying the variable's scope.

  • When scope is set to private, the variable's scope is the current node's private scope node.privateScope
  • When scope is set to public, the variable's scope is the current node's scope node.scope
form-meta.ts
import {
  createEffectFromVariableProvider,
  ASTFactory,
} from '@flowgram.ai/fixed-layout-editor';

export const formMeta =  {
  effect: {
    // Create variable in privateScope
    // = node.privateScope.setVar('path.to.value', ASTFactory.createVariableDeclaration(parse(v)))
    'path.to.value': createEffectFromVariableProvider({
      scope: 'private',
      // parse form value to variable
      parse(v: string, { node }) {
        return [{
          meta: {
            title: `Private_${v}`,
          },
          key: `uid_${node.id}_locals`,
          type: ASTFactory.createBoolean(),
        }]
      }
    }),
  },
  render: () => (
    // ...
  )
}

Method 2: node.privateScope

The API design of node.privateScope is almost identical to the node scope (node.scope), both providing methods like setVar, getVar, clearVar, etc., and both supporting namespaces. For details, please refer to node.scope.

form-meta.tsx
import { Effect } from '@flowgram.ai/editor';

export const formMeta = {
  effect: {
    'path.to.value': [{
      event: DataEvent.onValueInitOrChange,
      effect: ((params) => {
        const { context, value } = params;

        context.node.privateScope.setVar(
          ASTFactory.createVariableDeclaration({
            meta: {
              title: `Your Private Variable Title`,
            },
            key: `uid_${context.node.id}`,
            type: ASTFactory.createInteger(),
          })
        )

        console.log("View generated variables", context.node.privateScope.getVar())

      }) as Effect,
    }],
  },
  render: () => (
    // ...
  )
}

Output Global Variables

Global variables are like the “shared memory” of the entire flow—any node or plugin can read and modify them. They work well for state that persists across the flow, such as user information or environment configuration. (See Global Scope.)

When to choose the global scope
  • The variable is reused across multiple nodes or even plugins.
  • The variable should be decoupled from a specific node (e.g., environment config, user context).
  • You need to write it during initialization so downstream nodes can simply read it.

Similar to node variables, we also have two main ways to obtain the global variable scope (GlobalScope).

Method 1: Obtaining in Plugins

In the plugin's context (ctx), we can directly "inject" an instance of GlobalScope:

global-variable-plugin.tsx
import {
  GlobalScope,
  definePluginCreator,
  PluginCreator
} from '@flowgram.ai/fixed-layout-editor';

export const createGlobalVariablePlugin: PluginCreator<SyncVariablePluginOptions> =
  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({
    onInit(ctx, options) {
      const globalScope = ctx.get(GlobalScope)

      globalScope.setVar(
         ASTFactory.createVariableDeclaration({
          meta: {
            title: `Your Output Variable Title`,
          },
          key: `your_variable_global_unique_key`,
          type: ASTFactory.createString(),
        })
      )
    }
  })

Method 2: Obtaining in UI

If you want to interact with global variables in a React component on the canvas, you can use the useService Hook to obtain an instance of GlobalScope:

global-variable-component.tsx
import {
  GlobalScope,
  useService,
} from '@flowgram.ai/fixed-layout-editor';

function GlobalVariableComponent() {
  const globalScope = useService(GlobalScope)

  // ...

  const handleChange = (v: string) => {
    globalScope.setVar(
      ASTFactory.createVariableDeclaration({
        meta: {
          title: `Your Output Variable Title`,
        },
        key: `uid_${v}`,
        type: ASTFactory.createString(),
      })
    )
  }

  return <Input onChange={handleChange}/>
}

Global Scope API

The API design of GlobalScope is almost identical to the node scope (node.scope), both providing methods like setVar, getVar, clearVar, etc., and both supporting namespaces. For details, please refer to node.scope.

Here's a comprehensive example of operating global variables in a plugin:

sync-variable-plugin.tsx
import {
  GlobalScope,
} from '@flowgram.ai/fixed-layout-editor';

// ...

onInit(ctx, options) {
  const globalScope = ctx.get(GlobalScope);

  // 1. Create, Update, Read, Delete Variable in GlobalScope
  globalScope.setVar(
    ASTFactory.createVariableDeclaration({
      meta: {
        title: `Your Output Variable Title`,
      },
      key: `your_variable_global_unique_key`,
      type: ASTFactory.createString(),
    })
  )

  console.log(globalScope.getVar())

  globalScope.clearVar()

  // 2. Create, Update, Read, Delete Variable in GlobalScope's namespace: 'namespace_1'
    globalScope.setVar(
      'namespace_1',
      ASTFactory.createVariableDeclaration({
        meta: {
          title: `Your Output Variable Title 2`,
        },
        key: `uid_2`,
        type: ASTFactory.createString(),
      })
  )

  console.log(globalScope.getVar('namespace_1'))

  globalScope.clearVar('namespace_1')

  // ...
}

See: Class: GlobalScope