SuperAPI AI Directory

workflow-interactive-dev

Office & productivity

View repository

Updated 10-16 00:30

About this skill

Interaction Node Development Guide

Overview

FastGPT workflows support multiple interaction node types, allowing the workflow to pause during execution and wait for user input. This guide explains in detail how to develop new interaction nodes.

Existing Interaction Node Types

The current system supports the following interaction node types:

  1. userSelect - User selection node (single choice)
  2. formInput - Form input node (multi-field form)
  3. childrenInteractive - Sub-workflow interaction
  4. loopInteractive - Loop interaction
  5. paymentPause - Insufficient balance pause interaction

Interaction Node Architecture

Core Type Definitions

The type definitions for interaction nodes are located in packages/global/core/workflow/template/system/interactive/type.d.ts

// 基础交互结构
type InteractiveBasicType = {
  entryNodeIds: string[];                    // 入口节点ID列表
  memoryEdges: RuntimeEdgeItemType[];        // 需要记忆的边
  nodeOutputs: NodeOutputItemType[];         // 节点输出
  skipNodeQueue?: Array;                     // 跳过的节点队列
  usageId?: string;                          // 用量记录ID
};

// 具体交互节点类型
type YourInteractiveNode = InteractiveNodeType & {
  type: 'yourNodeType';
  params: {
    // 节点特定参数
  };
};

Workflow Execution Mechanism

Special handling of interaction nodes in workflow execution (located in packages/service/core/workflow/dispatch/index.ts:1012-1019):

// 部分交互节点不会自动重置 isEntry 标志(因为需要根据 isEntry 字段来判断是首次进入还是流程进入)
runtimeNodes.forEach((item) => {
  if (
    item.flowNodeType !== FlowNodeTypeEnum.userSelect &&
    item.flowNodeType !== FlowNodeTypeEnum.formInput &&
    item.flowNodeType !== FlowNodeTypeEnum.agent
  ) {
    item.isEntry = false;
  }
});

Steps to Develop a New Interactive Response

Step 1: Define the Node Type

File: packages/global/core/workflow/template/system/interactive/type.d.ts

export type YourInputItemType = {
  // 定义输入项的结构
  key: string;
  label: string;
  value: any;
  // ... 其他字段
};

type YourInteractiveNode = InteractiveNodeType & {
  type: 'yourNodeType';
  params: {
    description: string;
    yourInputField: YourInputItemType[];
    submitted?: boolean;  // 可选:是否已提交
  };
};

// 添加到联合类型
export type InteractiveNodeResponseType =
  | UserSelectInteractive
  | UserInputInteractive
  | YourInteractiveNode  // 新增
  | ChildrenInteractive
  | LoopInteractive
  | PaymentPauseInteractive;

Step 2: Define the Node Enum (Optional)

File: packages/global/core/workflow/node/constant.ts

If you do not need to add a new node type, you do not need to modify this file.

export enum FlowNodeTypeEnum {
  // ... 现有类型
  yourNodeType = 'yourNodeType',  // 新增节点类型
}

Step 3: Create the Node Template (Optional)

File: packages/global/core/workflow/template/system/interactive/yourNode.ts

import { i18nT } from '../../../../../../web/i18n/utils';
import {
  FlowNodeTemplateTypeEnum,
  NodeInputKeyEnum,
  NodeOutputKeyEnum,
  WorkflowIOValueTypeEnum
} from '../../../constants';
import {
  FlowNodeInputTypeEnum,
  FlowNodeOutputTypeEnum,
  FlowNodeTypeEnum
} from '../../../node/constant';
import { type FlowNodeTemplateType } from '../../../type/node';

export const YourNode: FlowNodeTemplateType = {
  id: FlowNodeTypeEnum.yourNodeType,
  templateType: FlowNodeTemplateTypeEnum.interactive,
  flowNodeType: FlowNodeTypeEnum.yourNodeType,
  showSourceHandle: true,   // 是否显示源连接点
  showTargetHandle: true,   // 是否显示目标连接点
  avatar: 'core/workflow/template/yourNode',
  name: i18nT('app:workflow.your_node'),
  intro: i18nT('app:workflow.your_node_tip'),
  isTool: true,  // 标记为工具节点
  inputs: [
    {
      key: NodeInputKeyEnum.description,
      renderTypeList: [FlowNodeInputTypeEnum.textarea],
      valueType: WorkflowIOValueTypeEnum.string,
      label: i18nT('app:workflow.node_description'),
      placeholder: i18nT('app:workflow.your_node_placeholder')
    },
    {
      key: NodeInputKeyEnum.yourInputField,
      renderTypeList: [FlowNodeInputTypeEnum.custom],
      valueType: WorkflowIOValueTypeEnum.any,
      label: '',
      value: []  // 默认值
    }
  ],
  outputs: [
    {
      id: NodeOutputKeyEnum.yourResult,
      key: NodeOutputKeyEnum.yourResult,
      required: true,
      label: i18nT('workflow:your_result'),
      valueType: WorkflowIOValueTypeEnum.object,
      type: FlowNodeOutputTypeEnum.static
    }
  ]
};

Step 4: Create the Node Execution Logic or Add New Logic to Nodes That Need to Handle Interaction Logic

File: packages/service/core/workflow/dispatch/interactive/yourNode.ts

import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import type {
  DispatchNodeResultType,
  ModuleDispatchProps
} from '@fastgpt/global/core/workflow/runtime/type';
import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { YourInputItemType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';

type Props = ModuleDispatchProps<{
  [NodeInputKeyEnum.description]: string;
  [NodeInputKeyEnum.yourInputField]: YourInputItemType[];
}>;

type YourNodeResponse = DispatchNodeResultType<{
  [NodeOutputKeyEnum.yourResult]?: Record<string, any>;
}>;

export const dispatchYourNode = async (props: Props): Promise<YourNodeResponse> => {
  const {
    histories,
    node,
    params: { description, yourInputField },
    query,
    lastInteractive
  } = props;
  const { isEntry } = node;

  // 第一阶段:非入口节点或不是对应的交互类型,返回交互请求
  if (!isEntry || lastInteractive?.type !== 'yourNodeType') {
    return {
      [DispatchNodeResponseKeyEnum.interactive]: {
        type: 'yourNodeType',
        params: {
          description,
          yourInputField
        }
      }
    };
  }

  // 第二阶段:处理用户提交的数据
  node.isEntry = false;  // 重要:重置入口标志

  const { text } = chatValue2RuntimePrompt(query);
  const userInputVal = (() => {
    try {
      return JSON.parse(text);  // 根据实际格式解析
    } catch (error) {
      return {};
    }
  })();

  return {
    data: {
      [NodeOutputKeyEnum.yourResult]: userInputVal
    },
    // 移除当前交互的历史记录(最后2条)
    [DispatchNodeResponseKeyEnum.rewriteHistories]: histories.slice(0, -2),
    [DispatchNodeResponseKeyEnum.toolResponses]: userInputVal,
    [DispatchNodeResponseKeyEnum.nodeResponse]: {
      yourResult: userInputVal
    }
  };
};

Step 5: Register the Node Callback

File: packages/service/core/workflow/dispatch/constants.ts

import { dispatchYourNode } from './interactive/yourNode';

export const callbackMap: Record<FlowNodeTypeEnum, any> = {
  // ... 现有节点
  [FlowNodeTypeEnum.yourNodeType]: dispatchYourNode,
};

Step 6: Create the Frontend Rendering Component

6.1 Chat Interface Interaction Component

File: projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx

export const YourNodeComponent = React.memo(function YourNodeComponent({
  interactiveParams: { description, yourInputField, submitted },
  defaultValues = {},
  SubmitButton
}: {
  interactiveParams: YourInteractiveNode['params'];
  defaultValues?: Record<string, any>;
  SubmitButton: (e: { onSubmit: UseFormHandleSubmit<Record<string, any>> }) => React.JSX.Element;
}) {
  const { handleSubmit, control } = useForm({
    defaultValues
  });

  return (
    <Box>
      <DescriptionBox description={description} />
      <Flex flexDirection={'column'} gap={3}>
        {yourInputField.map((input) => (
          <Box key={input.key}>
            {/* 渲染你的输入组件 */}
            <Controller
              control={control}
              name={input.key}
              render={({ field: { onChange, value } }) => (
                <YourInputComponent
                  value={value}
                  onChange={onChange}
                  isDisabled={submitted}
                />
              )}
            />
          </Box>
        ))}
      </Flex>

      {!submitted && (
        <Flex justifyContent={'flex-end'} mt={4}>
          <SubmitButton onSubmit={handleSubmit} />
        </Flex>
      )}
    </Box>
  );
});

6.2 Workflow Editor Node Component

File: projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeYourNode.tsx

import React, { useMemo } from 'react';
import { type NodeProps } from 'reactflow';
import { Box, Button } from '@chakra-ui/react';
import NodeCard from './render/NodeCard';
import { type FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { type FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { useContextSelector } from 'use-context-selector';
import IOTitle from '../components/IOTitle';
import RenderOutput from './render/RenderOutput';
import { WorkflowActionsContext } from '../../context/workflowActionsContext';

const NodeYourNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
  const { t } = useTranslation();
  const { nodeId, inputs, outputs } = data;
  const onChangeNode = useContextSelector(WorkflowActionsContext, (v) => v.onChangeNode);

  const CustomComponent = useMemo(
    () => ({
      [NodeInputKeyEnum.yourInputField]: (v: FlowNodeInputItemType) => {
        // 自定义渲染逻辑
        return (
          <Box>
            {/* 你的自定义UI */}
          </Box>
        );
      }
    }),
    [nodeId, onChangeNode, t]
  );

  return (
    <NodeCard minW={'400px'} selected={selected} {...data}>
      <Container>
        <RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
      </Container>
      <Container>
        <IOTitle text={t('common:Output')} />
        <RenderOutput nodeId={nodeId} flowOutputList={outputs} />
      </Container>
    </NodeCard>
  );
};

export default React.memo(NodeYourNode);

Step 7: Register the Node Component

You need to add your node component to the node registry (the specific location depends on the project configuration).

Step 8: Add Internationalization

File: packages/web/i18n/zh-CN/app.json and other language files

{
  "workflow": {
    "your_node": "你的节点名称",
    "your_node_tip": "节点功能说明",
    "your_node_placeholder": "提示文本"
  }
}

Step 9: Adjust the Logic for Saving Conversation Records

File: FastGPT/packages/service/core/chat/saveChat.ts

Modify the updateInteractiveChat method to support the new interaction

Step 10: Get/Set Interaction State Based on History Records

File: FastGPT/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts File: FastGPT/packages/global/core/workflow/runtime/utils.ts

AdjustsetInteractiveResultToHistories, getInteractiveByHistories and getLastInteractiveValuemethods.

Key Considerations

1. isEntry Flag Management

Interaction nodes need to keep the isEntry flag valid when the workflow resumes:

// 在 packages/service/core/workflow/dispatch/index.ts 中
// 确保你的节点类型被添加到白名单
if (
  item.flowNodeType !== FlowNodeTypeEnum.userSelect &&
  item.flowNodeType !== FlowNodeTypeEnum.formInput &&
  item.flowNodeType !== FlowNodeTypeEnum.yourNodeType  // 新增
) {
  item.isEntry = false;
}

2. Interaction Response Flow

Interaction nodes have two execution stages:

  1. First execution: return interactive response, pause the workflow
  2. Second execution: receive user input, continue the workflow
// 第一阶段
if (!isEntry || lastInteractive?.type !== 'yourNodeType') {
  return {
    [DispatchNodeResponseKeyEnum.interactive]: {
      type: 'yourNodeType',
      params: { /* ... */ }
    }
  };
}

// 第二阶段
node.isEntry = false;  // 重要!重置标志
// 处理用户输入...

3. History Record Management

Interaction nodes need to correctly handle history records:

return {
  // 移除交互对话的历史记录(用户问题 + 系统响应)
  [DispatchNodeResponseKeyEnum.rewriteHistories]: histories.slice(0, -2),
  // ... 其他返回值
};

4. Skip Node Queue

When an interaction node is triggered, the system saves skipNodeQueue so that already processed nodes are skipped when resuming.

5. Tool Call Support

If the node needs to be used in a tool call, set isTool: true。

Test Checklist

After development is complete, please test the following scenarios:

  • The node displays normally in the workflow editor
  • The node configuration is saved and loaded correctly
  • The interaction request is correctly sent to the frontend
  • The frontend component correctly renders the interactive interface
  • User input is correctly passed back to the backend
  • The workflow correctly resumes and continues execution
  • History records are correctly updated
  • Node output is correctly connected to subsequent nodes
  • Error cases are handled correctly
  • Multilingual support is complete

Reference Implementation

You can refer to the following existing implementations:

  1. Simple Single Choice: userSelect Node

    • Type definition: packages/global/core/workflow/template/system/interactive/type.d.ts:48-55
    • Execution logic: packages/service/core/workflow/dispatch/interactive/userSelect.ts
    • Frontend component: projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx:29-63
  2. Complex Form: formInput Node

    • Type definition: packages/global/core/workflow/template/system/interactive/type.d.ts:57-82
    • Execution logic: packages/service/core/workflow/dispatch/interactive/formInput.ts
    • Frontend component: projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx:65-126

Common Questions

Q: The interactive node executed twice?

A: This is normal. The first time it returns the interaction request, and the second time it processes user input. Make sure to set it during the second execution node.isEntry = false。

Q: The workflow did not continue executing after resuming?

A: Check whether your node type is in the isEntry whitelist (dispatch/index.ts:1013-1018).

Q: The user input format is incorrect?

A: Check chatValue2RuntimePrompt return value and parse it according to your data format.

Q: How to support multiple interactive nodes connected in series?

A: Each interactive node will pause the workflow, and after the user completes it, it will automatically continue to the next node.

File Checklist Summary

Developing a new interactive node requires modifying/creating the following files:

Backend Core Files

  1. packages/global/core/workflow/template/system/interactive/type.d.ts - Type definition
  2. packages/global/core/workflow/node/constant.ts - Node enum
  3. packages/global/core/workflow/template/system/interactive/yourNode.ts - Node template
  4. packages/service/core/workflow/dispatch/interactive/yourNode.ts - Execution logic
  5. packages/service/core/workflow/dispatch/constants.ts - Callback registration
  6. packages/service/core/workflow/dispatch/index.ts - isEntry whitelist

Frontend Component Files

  1. projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx - Chat interaction component
  2. projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeYourNode.tsx - Workflow editor component

Internationalization Files

  1. packages/web/i18n/zh-CN/app.json - Chinese translation
  2. packages/web/i18n/en/app.json - English translation
  3. packages/web/i18n/zh-Hant/app.json - Traditional Chinese translation

Appendix: Key Input/Output Key Definitions

If you need new input/output keys, define them in the following files:

File: packages/global/core/workflow/constants.ts

export enum NodeInputKeyEnum {
  // ... 现有键
  yourInputKey = 'yourInputKey',
}

export enum NodeOutputKeyEnum {
  // ... 现有键
  yourOutputKey = 'yourOutputKey',
}