Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6978ebf
Migrate Custom Assistants to Agents with single-table architecture
HenryHengZJ Apr 6, 2026
6ef68b3
- Add down() rollback logic to all 4 DB migration files to reverse ag…
HenryHengZJ Apr 6, 2026
f93ce86
Merge branch 'main' into feature/Migrate-Custom-Assistants-to-Agents
HenryHengZJ Apr 6, 2026
8f2c1fc
implement pagination for agents list
HenryHengZJ Apr 7, 2026
6befe25
Merge branch 'main' into feature/Migrate-Custom-Assistants-to-Agents
yau-wd Apr 7, 2026
fa9f0ff
Merge branch 'feature/Migrate-Custom-Assistants-to-Agents' of https:/…
HenryHengZJ Apr 11, 2026
07cf359
Merge branch 'main' into feature/Migrate-Custom-Assistants-to-Agents
HenryHengZJ Apr 11, 2026
d1a7566
Bugfix/add compact table transformer for NotionAPILoader (#6187)
HenryHengZJ Apr 11, 2026
4c0223a
Bugfix/add groq sdk (#6194)
HenryHengZJ Apr 11, 2026
38e7e49
Merge branch 'main' into feature/Migrate-Custom-Assistants-to-Agents
HenryHengZJ Apr 11, 2026
6ba035c
Chore/include action metadata from API responses (#6204)
HenryHengZJ Apr 13, 2026
0e52877
Bugfix/Silent fallback to vm2 for sandbox execution (#6206)
HenryHengZJ Apr 13, 2026
b27e889
feat(components): improve Baidu Wenxin chat model configuration (#6140)
jimmyzhuu Apr 13, 2026
faaaa11
feat(components): add Baidu Qianfan embeddings node (#6147)
jimmyzhuu Apr 13, 2026
7de9e3e
fix/agentflow: Update default values and auto-add Start node for empt…
jocelynlin-wd Apr 13, 2026
4114b3b
Release/3.1.2 (#6215)
HenryHengZJ Apr 14, 2026
311a3f4
Fix clickjacking (#6185)
yau-wd Apr 14, 2026
32232c0
fix(agentflow): add onFlowChange notifications for deleteNode, delete…
jocelynlin-wd Apr 14, 2026
04b897a
Fix: Clean and Nuke Script (#6213)
abdullah-workday Apr 14, 2026
96fca43
feat(agentflow): add client filtering for form input options in Start…
jocelynlin-wd Apr 14, 2026
62a5d1d
feat: turn chatflow into MCP server (#5930)
prd-hoang-doan Apr 15, 2026
a77194c
feat(agentflow): optional cavasActions to allow additional buttons ne…
jocelynlin-wd Apr 15, 2026
65f6805
chore: bump @flowiseai/agentflow to 0.0.0-dev.11 (#6216)
github-actions[bot] Apr 15, 2026
73d57db
chore: bump @flowiseai/agentflow to 0.0.0-dev.12 (#6225)
github-actions[bot] Apr 15, 2026
85df44b
Feat/FlowConfigDialog UI Redesign (#6229)
HenryHengZJ Apr 16, 2026
63ea77c
Merge branch 'main' into feature/Migrate-Custom-Assistants-to-Agents
HenryHengZJ Apr 16, 2026
b0cb4b0
- revamp agents and agentflows view pages
HenryHengZJ Apr 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,5 @@ apps/*/
# Claude - session/user specific files
.claude/plans/
.claude/settings.local.json
.claude/agent-memory/*
.claude/agent-memory/*
.claude/launch.json
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ Flowise support different environment variables to configure your instance. You
| PORT | The HTTP port Flowise runs on | Number | 3000 |
| CORS_ALLOW_CREDENTIALS | Enables CORS `Access-Control-Allow-Credentials` when `true` | Boolean | false |
| CORS_ORIGINS | The allowed origins for all cross-origin HTTP calls | String | |
| MCP_CORS_ORIGINS | The allowed origins for MCP endpoint cross-origin calls. If unset, only non-browser (no Origin header) requests are allowed. Set to `*` to allow all origins. | String | |
| IFRAME_ORIGINS | The allowed origins for iframe src embedding | String | |
| FLOWISE_FILE_SIZE_LIMIT | Upload File Size Limit | String | 50mb |
| DEBUG | Print logs from components | Boolean | |
Expand Down
1 change: 1 addition & 0 deletions docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ PORT=3000
# NUMBER_OF_PROXIES= 1
# CORS_ALLOW_CREDENTIALS=false
# CORS_ORIGINS=*
# MCP_CORS_ORIGINS=*
# IFRAME_ORIGINS=*
# FLOWISE_FILE_SIZE_LIMIT=50mb
# SHOW_COMMUNITY_NODES=true
Expand Down
1 change: 1 addition & 0 deletions docker/docker-compose-queue-prebuilt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ services:
# SETTINGS
- NUMBER_OF_PROXIES=${NUMBER_OF_PROXIES}
- CORS_ORIGINS=${CORS_ORIGINS}
- MCP_CORS_ORIGINS=${MCP_CORS_ORIGINS}
- IFRAME_ORIGINS=${IFRAME_ORIGINS}
- FLOWISE_FILE_SIZE_LIMIT=${FLOWISE_FILE_SIZE_LIMIT}
- SHOW_COMMUNITY_NODES=${SHOW_COMMUNITY_NODES}
Expand Down
1 change: 1 addition & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ services:
# SETTINGS
- NUMBER_OF_PROXIES=${NUMBER_OF_PROXIES}
- CORS_ORIGINS=${CORS_ORIGINS}
- MCP_CORS_ORIGINS=${MCP_CORS_ORIGINS}
- IFRAME_ORIGINS=${IFRAME_ORIGINS}
- FLOWISE_FILE_SIZE_LIMIT=${FLOWISE_FILE_SIZE_LIMIT}
- SHOW_COMMUNITY_NODES=${SHOW_COMMUNITY_NODES}
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "flowise",
"version": "3.1.1",
"version": "3.1.2",
"private": true,
"homepage": "https://flowiseai.com",
"workspaces": [
Expand All @@ -25,8 +25,8 @@
"user:default": "cd packages/server/bin && ./run user",
"test": "turbo run test",
"test:coverage": "turbo run test:coverage",
"clean": "pnpm --filter \"./packages/**\" clean",
"nuke": "pnpm --filter \"./packages/**\" nuke && rimraf node_modules .turbo",
"clean": "pnpm -r clean",
"nuke": "pnpm -r nuke && rimraf node_modules .turbo",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"lint": "eslint \"**/*.{js,jsx,ts,tsx,json,md}\"",
"lint-fix": "pnpm lint --fix",
Expand Down
10 changes: 9 additions & 1 deletion packages/agentflow/examples/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
DarkModeExampleProps,
FilteredComponentsExampleProps,
MultiNodeFlowProps,
StatusIndicatorsExampleProps
StatusIndicatorsExampleProps,
ValidationActionsExampleProps
} from './demos'
import { PropsDisplay } from './PropsDisplay'

Expand Down Expand Up @@ -81,6 +82,13 @@ const examples: Array<{
description: 'Restrict available nodes with presets',
props: FilteredComponentsExampleProps,
component: lazy(() => import('./demos/FilteredComponentsExample').then((m) => ({ default: m.FilteredComponentsExample })))
},
{
id: 'canvas-actions',
name: 'Canvas Actions',
description: 'Custom FABs alongside the validation button via canvasActions',
props: ValidationActionsExampleProps,
component: lazy(() => import('./demos/ValidationActionsExample').then((m) => ({ default: m.ValidationActionsExample })))
}
]

Expand Down
24 changes: 2 additions & 22 deletions packages/agentflow/examples/src/demos/CustomUIExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,6 @@ import { Agentflow } from '@flowiseai/agentflow'

import { apiBaseUrl, token } from '../config'

const initialFlow: FlowData = {
nodes: [
{
id: 'startAgentflow_0',
type: 'agentflowNode',
position: { x: 300, y: 200 },
data: {
id: 'startAgentflow_0',
name: 'startAgentflow',
label: 'Start',
color: '#7EE787',
hideInput: true,
outputAnchors: [{ id: 'startAgentflow_0-output-0', name: 'start', label: 'Start', type: 'start' }]
}
}
],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 }
}

// Custom header component
function CustomHeader({ flowName, isDirty, onSave, onExport, onValidate }: HeaderRenderProps) {
const [validationStatus, setValidationStatus] = useState<'valid' | 'invalid' | null>(null)
Expand Down Expand Up @@ -259,11 +239,11 @@ export function CustomUIExample() {
ref={agentflowRef}
apiBaseUrl={apiBaseUrl}
token={token ?? undefined}
initialFlow={initialFlow}
renderHeader={(props: HeaderRenderProps) => <CustomHeader {...props} />}
renderNodePalette={(props: PaletteRenderProps) => <CustomPalette {...props} />}
showDefaultHeader={false}
showDefaultPalette={false}
enableGenerator={false}
onSave={(flow: FlowData) => {
console.log('Saving flow:', flow)
alert('Flow saved! Check console.')
Expand All @@ -277,10 +257,10 @@ export function CustomUIExample() {
export const CustomUIExampleProps = {
apiBaseUrl: '{from environment variables}',
token: '{from environment variables}',
initialFlow: 'FlowData',
renderHeader: '(props: HeaderRenderProps) => ReactNode',
renderNodePalette: '(props: PaletteRenderProps) => ReactNode',
showDefaultHeader: false,
showDefaultPalette: false,
enableGenerator: false,
onSave: '(flow: FlowData) => void'
}
71 changes: 71 additions & 0 deletions packages/agentflow/examples/src/demos/ValidationActionsExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Canvas Actions Example
*
* Demonstrates how to add custom FAB buttons alongside the built-in validation
* button in the top-right canvas overlay using the `canvasActions` prop.
*
* Mirrors the legacy v2 pattern where a chat FAB and validation FAB sit side-by-side.
*/

import { useState } from 'react'

import { Agentflow } from '@flowiseai/agentflow'
import { Box, Dialog, DialogContent, DialogTitle, Fab, IconButton, Typography } from '@mui/material'
import { IconMessage, IconX } from '@tabler/icons-react'

import { apiBaseUrl, token } from '../config'

function ChatFab() {
const [open, setOpen] = useState(false)

return (
<>
<Fab
size='small'
aria-label='chat'
title='Chat'
onClick={() => setOpen(true)}
sx={{
color: 'white',
backgroundColor: 'secondary.main',
'&:hover': {
backgroundColor: 'secondary.main',
backgroundImage: 'linear-gradient(rgb(0 0 0/10%) 0 0)'
}
}}
>
<IconMessage />
</Fab>

<Dialog open={open} onClose={() => setOpen(false)} maxWidth='sm' fullWidth>
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
Chat
<IconButton size='small' onClick={() => setOpen(false)}>
<IconX size={18} />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{ py: 4, textAlign: 'center' }}>
<Typography variant='body2' color='text.secondary'>
Your chat UI goes here. Full control — bring any component.
</Typography>
</Box>
</DialogContent>
</Dialog>
</>
)
}

export function ValidationActionsExample() {
return (
<div style={{ width: '100%', height: '100%' }}>
<Agentflow apiBaseUrl={apiBaseUrl} token={token ?? undefined} canvasActions={<ChatFab />} />
</div>
)
}

export const ValidationActionsExampleProps = {
apiBaseUrl: '{from environment variables}',
token: '{from environment variables}',
canvasActions: '<ChatFab />'
}
1 change: 1 addition & 0 deletions packages/agentflow/examples/src/demos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './DarkModeExample'
export * from './FilteredComponentsExample'
export * from './MultiNodeFlow'
export * from './StatusIndicatorsExample'
export * from './ValidationActionsExample'
4 changes: 2 additions & 2 deletions packages/agentflow/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flowiseai/agentflow",
"version": "0.0.0-dev.10",
"version": "0.0.0-dev.12",
"description": "Embeddable React component for building and visualizing AI agent workflows",
"license": "Apache-2.0",
"repository": {
Expand Down Expand Up @@ -55,7 +55,7 @@
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"nuke": "rimraf dist node_modules .turbo",
"nuke": "pnpm clean && rimraf node_modules .turbo",
"prepublishOnly": "npm run clean && npm run build"
},
"peerDependencies": {
Expand Down
131 changes: 130 additions & 1 deletion packages/agentflow/src/Agentflow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
import { createRef } from 'react'

import { fireEvent, render, waitFor } from '@testing-library/react'
import mockAxios from 'axios'

import type { AgentFlowInstance, FlowData } from './core/types'
import type { AgentFlowInstance, AgentflowProps, FlowData } from './core/types'
import { Agentflow } from './Agentflow'

// Mock external dependencies - implementations in __mocks__/
jest.mock('reactflow')
jest.mock('axios')

const mockGet = mockAxios.get as jest.Mock

// Mock GenerateFlowDialog to expose callbacks for testing
jest.mock('./features/generator', () => ({
GenerateFlowDialog: ({
Expand Down Expand Up @@ -425,6 +428,30 @@ describe('Agentflow Component', () => {
})
})

describe('Canvas Actions', () => {
it('should render canvasActions content in the canvas', async () => {
const { getByTestId } = render(
<Agentflow {...defaultProps} canvasActions={<button data-testid='custom-action'>My Button</button>} />
)

await waitFor(() => {
expect(getByTestId('custom-action')).toBeInTheDocument()
})
})

it('should not render canvasActions in read-only mode', async () => {
const { container, queryByTestId } = render(
<Agentflow {...defaultProps} readOnly={true} canvasActions={<button data-testid='custom-action'>My Button</button>} />
)

await waitFor(() => {
expect(container.querySelector('.agentflow-container')).toBeInTheDocument()
})

expect(queryByTestId('custom-action')).not.toBeInTheDocument()
})
})

describe('Imperative Ref', () => {
it('should expose agentflow instance via ref', async () => {
const ref = createRef<AgentFlowInstance>()
Expand Down Expand Up @@ -482,4 +509,106 @@ describe('Agentflow Component', () => {
expect(document.getElementById('agentflow-css-variables')).not.toBeInTheDocument()
})
})

describe('Auto Start Node Initialization', () => {
const startNodeSchema = {
name: 'startAgentflow',
label: 'Start',
type: 'Start',
category: 'Agent Flows',
description: 'Start node',
baseClasses: ['Start'],
inputs: [],
outputs: [],
version: 1
}

beforeEach(() => {
// Mock API to return a startAgentflow node definition
mockGet.mockImplementation((url: string) => {
if (typeof url === 'string' && url.includes('/nodes')) {
return Promise.resolve({ data: [startNodeSchema] })
}
return Promise.resolve({ data: [] })
})
})

afterEach(() => {
// Restore default mock (empty array)
mockGet.mockImplementation(() => Promise.resolve({ data: [] }))
})

/** Helper: render without initialFlow and assert Start node via ref */
const renderAndGetNodes = async (initialFlow?: FlowData | null) => {
const ref = createRef<AgentFlowInstance>()
const props: Record<string, unknown> = { apiBaseUrl: 'https://example.com', ref }
if (initialFlow !== undefined) props.initialFlow = initialFlow

render(<Agentflow {...(props as unknown as AgentflowProps)} />)

await waitFor(() => {
expect(ref.current).toBeDefined()
const flow = ref.current!.getFlow()
expect(flow.nodes.length).toBeGreaterThan(0)
})
return ref.current!.getFlow().nodes
}

it('should auto-add Start node when initialFlow is undefined', async () => {
const nodes = await renderAndGetNodes()
expect(nodes).toHaveLength(1)
expect(nodes[0].id).toBe('startAgentflow_0')
expect(nodes[0].data.name).toBe('startAgentflow')
expect(nodes[0].data.label).toBe('Start')
})

it('should auto-add Start node when initialFlow is null', async () => {
const nodes = await renderAndGetNodes(null)
expect(nodes).toHaveLength(1)
expect(nodes[0].data.name).toBe('startAgentflow')
})

it('should auto-add Start node when initialFlow has empty nodes', async () => {
const emptyFlow: FlowData = { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } }
const nodes = await renderAndGetNodes(emptyFlow)
expect(nodes).toHaveLength(1)
expect(nodes[0].data.name).toBe('startAgentflow')
})

it('should auto-add Start node when initialFlow is empty object', async () => {
const nodes = await renderAndGetNodes({} as FlowData)
expect(nodes).toHaveLength(1)
expect(nodes[0].data.name).toBe('startAgentflow')
})

it('should handle initialFlow with unrelated fields without errors', async () => {
const illegalFlow = { foo: 'bar', baz: 123 } as unknown as FlowData
const nodes = await renderAndGetNodes(illegalFlow)

expect(nodes).toHaveLength(1)
expect(nodes[0].data.name).toBe('startAgentflow')
})

it('should handle initialFlow with wrong types for nodes/edges without crashing', async () => {
const illegalFlow = { nodes: 'not-an-array', edges: 42 } as unknown as FlowData
const nodes = await renderAndGetNodes(illegalFlow)

// Non-array nodes/edges are safely ignored, auto-init adds Start node
expect(nodes).toHaveLength(1)
expect(nodes[0].data.name).toBe('startAgentflow')
})

it('should not auto-add Start node when initialFlow has nodes', async () => {
const ref = createRef<AgentFlowInstance>()
render(<Agentflow {...defaultProps} initialFlow={mockFlow} ref={ref} />)

await waitFor(() => {
expect(ref.current).toBeDefined()
})
const nodes = ref.current!.getFlow().nodes
// Should only have the one node from mockFlow, no duplicate Start added
const startNodes = nodes.filter((n) => n.data.name === 'startAgentflow')
expect(startNodes).toHaveLength(1)
})
})
})
Loading