Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions packages/@n8n/api-types/src/chat-hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,28 @@ export class ChatHubSendMessageRequest extends Z.class({
timeZone: TimeZoneSchema,
}) {}

/**
* Request schema for sending a message via the manual (draft) execution path.
* Same shape as ChatHubSendMessageRequest — the endpoint itself signals "use draft from DB".
* Requires workflow:execute permission (not available to chat-only users).
*/
export class ChatHubManualSendMessageRequest extends Z.class({
messageId: z.string().uuid(),
sessionId: z.string().uuid(),
message: z.string(),
model: n8nModelSchema,
previousMessageId: z.string().uuid().nullable(),
credentials: z.record(
z.object({
id: z.string(),
name: z.string(),
}),
),
attachments: z.array(chatAttachmentSchema),
agentName: z.string().optional(),
timeZone: TimeZoneSchema,
}) {}

export class ChatHubRegenerateMessageRequest extends Z.class({
model: chatHubConversationModelSchema,
credentials: z.record(
Expand Down
1 change: 1 addition & 0 deletions packages/@n8n/api-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export {
chatAttachmentSchema,
type ChatAttachment,
ChatHubSendMessageRequest,
ChatHubManualSendMessageRequest,
ChatHubRegenerateMessageRequest,
ChatHubEditMessageRequest,
ChatHubUpdateConversationRequest,
Expand Down
16 changes: 15 additions & 1 deletion packages/cli/src/modules/chat-hub/chat-hub-execution.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,14 @@ export class ChatHubExecutionService {
previousMessageId: ChatMessageId,
retryOfMessageId: ChatMessageId | null,
responseMode: ChatTriggerResponseMode,
pushRef?: string,
) {
const executionMode = model.provider === 'n8n' ? 'webhook' : 'chat';
const executionMode =
pushRef && model.provider === 'n8n'
? 'manual'
: model.provider === 'n8n'
? 'webhook'
: 'chat';
const { id: workflowId } = workflowData;

try {
Expand All @@ -109,6 +115,7 @@ export class ChatHubExecutionService {
retryOfMessageId,
executionMode,
responseMode,
pushRef,
);
} catch (error) {
this.logger.error(`Error in chat execution: ${error}`);
Expand Down Expand Up @@ -153,6 +160,7 @@ export class ChatHubExecutionService {
retryOfMessageId: ChatMessageId | null,
executionMode: WorkflowExecuteMode,
responseMode: ChatTriggerResponseMode,
pushRef?: string,
) {
this.logger.debug(
`Starting execution of workflow "${workflowData.name}" with ID ${workflowData.id}`,
Expand All @@ -173,6 +181,7 @@ export class ChatHubExecutionService {
retryOfMessageId,
executionMode,
responseMode,
pushRef,
);
} else if (responseMode === 'streaming') {
return await this.executeWithStreaming(
Expand All @@ -184,6 +193,7 @@ export class ChatHubExecutionService {
previousMessageId,
retryOfMessageId,
executionMode,
pushRef,
);
}
}
Expand All @@ -200,6 +210,7 @@ export class ChatHubExecutionService {
previousMessageId: string,
retryOfMessageId: string | null,
executionMode: WorkflowExecuteMode,
pushRef?: string,
) {
let executionId: string | undefined;
let executionStatus: 'success' | 'error' | 'cancelled' = 'success';
Expand Down Expand Up @@ -310,6 +321,7 @@ export class ChatHubExecutionService {
streamAdapter,
true,
executionMode,
pushRef,
);

executionId = execution.executionId;
Expand Down Expand Up @@ -360,6 +372,7 @@ export class ChatHubExecutionService {
retryOfMessageId: string | null,
executionMode: WorkflowExecuteMode,
responseMode: NonStreamingResponseMode,
pushRef?: string,
) {
// 1. Start the workflow execution
const running = await this.workflowExecutionService.executeChatWorkflow(
Expand All @@ -369,6 +382,7 @@ export class ChatHubExecutionService {
undefined,
false,
executionMode,
pushRef,
);

const executionId = running.executionId;
Expand Down
35 changes: 21 additions & 14 deletions packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,7 @@ Respond the title only:`,
timeZone: string,
trx: EntityManager,
executionMetadata: ChatHubAuthenticationMetadata,
manual?: boolean,
): Promise<PreparedChatWorkflow> {
if (model.provider === 'n8n') {
return await this.prepareWorkflowAgentWorkflow(
Expand All @@ -1067,6 +1068,7 @@ Respond the title only:`,
attachments,
trx,
executionMetadata,
manual,
);
}

Expand Down Expand Up @@ -1203,21 +1205,29 @@ Respond the title only:`,
attachments: IBinaryData[],
trx: EntityManager,
executionMetadata: ChatHubAuthenticationMetadata,
manual?: boolean,
) {
const workflow = await this.workflowFinderService.findWorkflowForUser(
workflowId,
user,
['workflow:execute-chat'],
{ includeTags: false, includeParentFolder: false, includeActiveVersion: true, em: trx },
manual ? ['workflow:execute'] : ['workflow:execute-chat'],
{ includeTags: false, includeParentFolder: false, includeActiveVersion: !manual, em: trx },
);

if (!workflow?.activeVersion) {
if (!workflow) {
throw new BadRequestError('Workflow not found');
}

const chatTriggers = workflow.activeVersion.nodes.filter(
(node) => node.type === CHAT_TRIGGER_NODE_TYPE,
);
// In manual mode, use draft nodes/connections directly from the workflow.
// In normal mode, use the published activeVersion.
if (!manual && !workflow.activeVersion) {
throw new BadRequestError('Workflow not found');
}

const workflowNodes = manual ? workflow.nodes : workflow.activeVersion!.nodes;
const workflowConnections = manual ? workflow.connections : workflow.activeVersion!.connections;

const chatTriggers = workflowNodes.filter((node) => node.type === CHAT_TRIGGER_NODE_TYPE);

if (chatTriggers.length !== 1) {
throw new BadRequestError('Workflow must have exactly one chat trigger');
Expand Down Expand Up @@ -1247,19 +1257,15 @@ Respond the title only:`,
);
}

const chatResponseNodes = workflow.activeVersion.nodes.filter(
(node) => node.type === CHAT_NODE_TYPE,
);
const chatResponseNodes = workflowNodes.filter((node) => node.type === CHAT_NODE_TYPE);

if (chatResponseNodes.length > 0 && responseMode !== 'responseNodes') {
throw new BadRequestError(
'Chat nodes are not supported with the selected response mode. Please set the response mode to "Using Response Nodes" or remove the nodes from the workflow.',
);
}

const agentNodes = workflow.activeVersion.nodes?.filter(
(node) => node.type === AGENT_LANGCHAIN_NODE_TYPE,
);
const agentNodes = workflowNodes.filter((node) => node.type === AGENT_LANGCHAIN_NODE_TYPE);

// Agents older than this can't do streaming
if (agentNodes.some((node) => node.typeVersion < TOOLS_AGENT_NODE_MIN_VERSION)) {
Expand Down Expand Up @@ -1287,8 +1293,9 @@ Respond the title only:`,

const workflowData: IWorkflowBase = {
...workflow,
nodes: workflow.activeVersion.nodes,
connections: workflow.activeVersion.connections,
nodes: workflowNodes,
connections: workflowConnections,
pinData: manual ? (workflow.pinData ?? undefined) : undefined,
// Force saving data on successful executions for custom agent workflows
// to be able to read the results after execution.
settings: {
Expand Down
33 changes: 33 additions & 0 deletions packages/cli/src/modules/chat-hub/chat-hub.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ChatHubSendMessageRequest,
ChatHubManualSendMessageRequest,
ChatModelsResponse,
ChatHubConversationsResponse,
ChatHubConversationResponse,
Expand Down Expand Up @@ -153,6 +154,38 @@ export class ChatHubController {
};
}

/**
* Send a message using the draft (unpublished) workflow version.
* Requires workflow:execute — not available to chat-only users.
* Passes pushRef header so the execution sends canvas events.
*/
@GlobalScope('workflow:execute')
@Post('/conversations/send/manual')
async sendMessageManual(
req: AuthenticatedRequest,
_res: Response,
@Body payload: ChatHubManualSendMessageRequest,
): Promise<ChatSendMessageResponse> {
const pushRef = req.headers['push-ref'] as string | undefined;
if (!pushRef) {
throw new BadRequestError('push-ref header is required for manual execution');
}

await this.chatService.sendHumanMessageManual(
req.user,
{
...payload,
userId: req.user.id,
},
extractAuthenticationMetadata(req),
pushRef,
);

return {
status: 'streaming',
};
}

@GlobalScope('chatHub:message')
@Post('/conversations/:sessionId/messages/:messageId/edit')
async editMessage(
Expand Down
Loading