From e2dd10c4355d718682c82d6e76204d541fb65fcf Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Tue, 26 May 2026 13:15:00 +0200 Subject: [PATCH] feat(core): Move builder templates to n8n-sdk-templates with runtime fetch (#30537) Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Oleg Ivaniv --- packages/@n8n/instance-ai/src/index.ts | 8 + .../build-workflow-agent.tool.ts | 704 ++-- packages/@n8n/instance-ai/src/types.ts | 7 + .../builder-templates-service.test.ts | 647 +++ .../workspace/__tests__/sandbox-fs.test.ts | 20 + .../workspace/__tests__/sandbox-setup.test.ts | 282 +- .../src/workspace/builder-sandbox-factory.ts | 6 +- .../workspace/builder-templates-service.ts | 616 +++ .../instance-ai/src/workspace/sandbox-fs.ts | 5 +- .../src/workspace/sandbox-setup.ts | 180 +- .../src/workspace/template-telemetry.test.ts | 27 +- .../src/workspace/template-telemetry.ts | 28 +- packages/@n8n/workflow-sdk/.gitignore | 8 - .../workflow-sdk/docs/template-criteria.md | 95 - .../workflow-sdk/examples/_calibration.json | 245 -- .../examples/_coverage-report.json | 809 ---- .../@n8n/workflow-sdk/examples/manifest.json | 3491 ----------------- .../@n8n/workflow-sdk/examples/templates.zip | Bin 533128 -> 0 bytes packages/@n8n/workflow-sdk/package.json | 18 +- .../@n8n/workflow-sdk/scripts/calibration.ts | 179 - .../workflow-sdk/scripts/coverage.test.ts | 64 - .../@n8n/workflow-sdk/scripts/coverage.ts | 324 -- .../scripts/create-examples-zip.ts | 40 - .../workflow-sdk/scripts/criteria.test.ts | 452 --- .../@n8n/workflow-sdk/scripts/criteria.ts | 398 -- .../workflow-sdk/scripts/extract-workflows.ts | 11 +- .../workflow-sdk/scripts/fetch-templates.ts | 230 -- .../scripts/regenerate-examples.ts | 551 --- .../src/__tests__/examples-roundtrip.test.ts | 81 - .../workflow-sdk/src/examples-loader.test.ts | 50 - .../@n8n/workflow-sdk/src/examples-loader.ts | 142 - .../@n8n/workflow-sdk/src/examples-zip.ts | 107 - ...stance-ai.adapter.service.security.test.ts | 11 +- .../instance-ai.adapter.service.test.ts | 19 +- .../instance-ai.adapter.service.ts | 20 +- 35 files changed, 2207 insertions(+), 7668 deletions(-) create mode 100644 packages/@n8n/instance-ai/src/workspace/__tests__/builder-templates-service.test.ts create mode 100644 packages/@n8n/instance-ai/src/workspace/builder-templates-service.ts delete mode 100644 packages/@n8n/workflow-sdk/docs/template-criteria.md delete mode 100644 packages/@n8n/workflow-sdk/examples/_calibration.json delete mode 100644 packages/@n8n/workflow-sdk/examples/_coverage-report.json delete mode 100644 packages/@n8n/workflow-sdk/examples/manifest.json delete mode 100644 packages/@n8n/workflow-sdk/examples/templates.zip delete mode 100644 packages/@n8n/workflow-sdk/scripts/calibration.ts delete mode 100644 packages/@n8n/workflow-sdk/scripts/coverage.test.ts delete mode 100644 packages/@n8n/workflow-sdk/scripts/coverage.ts delete mode 100644 packages/@n8n/workflow-sdk/scripts/create-examples-zip.ts delete mode 100644 packages/@n8n/workflow-sdk/scripts/criteria.test.ts delete mode 100644 packages/@n8n/workflow-sdk/scripts/criteria.ts delete mode 100644 packages/@n8n/workflow-sdk/scripts/fetch-templates.ts delete mode 100644 packages/@n8n/workflow-sdk/scripts/regenerate-examples.ts delete mode 100644 packages/@n8n/workflow-sdk/src/__tests__/examples-roundtrip.test.ts delete mode 100644 packages/@n8n/workflow-sdk/src/examples-loader.test.ts delete mode 100644 packages/@n8n/workflow-sdk/src/examples-loader.ts delete mode 100644 packages/@n8n/workflow-sdk/src/examples-zip.ts diff --git a/packages/@n8n/instance-ai/src/index.ts b/packages/@n8n/instance-ai/src/index.ts index 66ed8236e7c..b122cf1a19c 100644 --- a/packages/@n8n/instance-ai/src/index.ts +++ b/packages/@n8n/instance-ai/src/index.ts @@ -238,6 +238,14 @@ export { createLazyRuntimeWorkspace } from './workspace/lazy-runtime-workspace'; export type { RuntimeWorkspaceResolver } from './workspace/lazy-runtime-workspace'; export { getWorkspaceRoot, setupSandboxWorkspace } from './workspace/sandbox-setup'; export type { BuilderWorkspace } from './workspace/builder-sandbox-factory'; +export { + BuilderTemplatesService, + builderTemplatesOptionsFromEnv, +} from './workspace/builder-templates-service'; +export type { + BuilderTemplatesBundle, + BuilderTemplatesServiceOptions, +} from './workspace/builder-templates-service'; export type BuilderSandboxFactory = BuilderSandboxFactoryMod.BuilderSandboxFactory; export const createSandbox: typeof CreateWorkspaceMod.createSandbox = lazyFunction( () => loadCreateWorkspace().createSandbox, diff --git a/packages/@n8n/instance-ai/src/tools/orchestration/build-workflow-agent.tool.ts b/packages/@n8n/instance-ai/src/tools/orchestration/build-workflow-agent.tool.ts index 6677767ddce..dcc3bbc2b2b 100644 --- a/packages/@n8n/instance-ai/src/tools/orchestration/build-workflow-agent.tool.ts +++ b/packages/@n8n/instance-ai/src/tools/orchestration/build-workflow-agent.tool.ts @@ -55,6 +55,13 @@ import { type SandboxWorkspace, } from '../../workspace/sandbox-fs'; import { getWorkspaceRoot } from '../../workspace/sandbox-setup'; +import { + attachTemplateTelemetrySession, + createTemplateTelemetrySession, + createTypedToolObserver, + detachTemplateTelemetrySession, + type TemplateTelemetrySession, +} from '../../workspace/template-telemetry'; import { CREDENTIALS_TOOL_ID, createCredentialsTool, @@ -1368,367 +1375,406 @@ export async function startBuildWorkflowAgentTask( const workspace = sharedWorkspace; const root = await getWorkspaceRoot(workspace); const builderLayout = builderWorkflowWorkspaceLayout(root, workItemId); + let telemetrySession: TemplateTelemetrySession | undefined; + let unsubscribeTelemetry: (() => void) | undefined; - prompt = createSandboxBuilderAgentPrompt(root, { - mainWorkflowPath: builderLayout.mainWorkflowPath, - sourceDir: builderLayout.sourceDir, - chunksDir: builderLayout.chunksDir, - tsconfigPath: builderLayout.tsconfigPath, - }); - await writeBuilderWorkspaceFile( - workspace, - builderLayout.tsconfigPath, - renderBuilderTaskTsconfig(), - ); + try { + telemetrySession = createTemplateTelemetrySession({ + context, + threadId: context.threadId, + runId: context.runId, + workItemId, + userRequestExcerpt: input.task, + templatesVersion: domainContext.templatesService?.getVersion() ?? null, + }); + attachTemplateTelemetrySession(workspace, telemetrySession); + const templateToolObserver = createTypedToolObserver(telemetrySession); + unsubscribeTelemetry = context.eventBus.subscribe(context.threadId, (stored) => { + if (stored.event.agentId !== subAgentId) return; + templateToolObserver(stored.event); + }); - if (workflowId) { - try { - const json = await domainContext.workflowService.getAsWorkflowJSON(workflowId); - const rawCode = generateWorkflowCode(json); - const code = `${SDK_IMPORT_STATEMENT}\n\n${rawCode}`; - await writeBuilderWorkspaceFile(workspace, builderLayout.mainWorkflowPath, code); - } catch { - // Non-fatal — agent can still build from scratch - } - } else { + prompt = createSandboxBuilderAgentPrompt(root, { + mainWorkflowPath: builderLayout.mainWorkflowPath, + sourceDir: builderLayout.sourceDir, + chunksDir: builderLayout.chunksDir, + tsconfigPath: builderLayout.tsconfigPath, + }); await writeBuilderWorkspaceFile( workspace, - builderLayout.mainWorkflowPath, - `${SDK_IMPORT_STATEMENT}\n\n`, + builderLayout.tsconfigPath, + renderBuilderTaskTsconfig(), ); - } - const mainWorkflowPath = builderLayout.mainWorkflowPath; - const initialMainWorkflowSnapshot = createMainWorkflowSnapshot( - await readFileViaSandbox(workspace, mainWorkflowPath), - ); - builderTools.set( - 'submit-workflow', - createIdentityEnforcedSubmitWorkflowTool({ - context: domainContext, - workspace, - credentialMap: credMap, - root, - defaultFilePath: mainWorkflowPath, - currentRunId: context.runId, - getWorkflowLoopState: async () => - await context.workflowTaskService?.getWorkflowLoopState(workItemId), - onGuardFired: (event) => { - context.trackTelemetry?.('Builder remediation guard fired', { - thread_id: context.threadId, - run_id: context.runId, - work_item_id: workItemId, - workflow_id: event.workflowId, - category: event.category, - attempt_count: event.attemptCount, - reason: event.reason, - }); - }, - onAttempt: async (attempt) => { - submitAttempts.set(attempt.filePath, attempt); - submitAttemptHistory.push(attempt); - if (attempt.filePath !== mainWorkflowPath) { - return; - } - if (!context.workflowTaskService) { - return; - } - - await context.workflowTaskService.reportBuildOutcome( - buildOutcome( - workItemId, - context.runId, - taskId, - attempt, - attempt.success - ? 'Workflow submitted and ready for verification.' - : (attempt.errors?.join(' ') ?? 'Workflow submission failed.'), - ), - ); - }, - }), - ); - - const tracedBuilderTools = traceSubAgentTools(context, builderTools, 'workflow-builder'); - const runtimeWorkspaceTools = toToolRegistry(workspace.getTools()); - const builderMemory = getBuilderSessionMemory(context, true); - const shouldUseBuilderMemory = Boolean(builderMemory); - - const subAgent = new Agent('Workflow Builder Agent') - .model(context.modelId) - .instructions(prompt, { - providerOptions: { - anthropic: { cacheControl: { type: 'ephemeral' } }, - }, - }) - .tool(toolRegistryValues(tracedBuilderTools)) - .workspace(workspace) - .checkpoint(context.checkpointStore ?? 'memory'); - if (builderMemory) { - subAgent.memory(builderMemory); - } - const telemetry = traceContext?.getTelemetry?.({ - agentRole: 'workflow-builder', - functionId: 'instance-ai.subagent.workflow-builder', - executionMode: 'background_subagent', - metadata: { agent_id: subAgentId, task_id: taskId }, - }); - if (telemetry) { - subAgent.telemetry(telemetry); - } - mergeTraceRunInputs( - traceContext?.actorRun, - buildAgentTraceInputs({ - systemPrompt: prompt, - tools: tracedBuilderTools, - runtimeTools: runtimeWorkspaceTools, - modelId: context.modelId, - }), - ); - - let finalText: string; - try { - const persistence = await createSubAgentPersistence(context, { - agentKind: 'workflow-builder', - threadId: builderThreadId, - resourceId: builderResourceId, - }); - const resumeOptions: Record = { - providerOptions: { - anthropic: { cacheControl: { type: 'ephemeral' } }, - }, - }; - const stream = await subAgent.stream(briefing, { - maxIterations: MAX_STEPS.BUILDER, - abortSignal: signal, - persistence, - providerOptions: { - anthropic: { cacheControl: { type: 'ephemeral' } }, - }, - }); - - const hitlResult = await consumeStreamWithHitl({ - agent: subAgent, - stream, - runId: context.runId, - agentId: subAgentId, - eventBus: context.eventBus, - logger: context.logger, - threadId: context.threadId, - abortSignal: signal, - waitForConfirmation: context.waitForConfirmation, - drainCorrections, - waitForCorrection, - maxIterations: MAX_STEPS.BUILDER, - resumeOptions, - persistence, - }); - - finalText = await requireCompletedHitlText(hitlResult, 'Workflow builder sub-agent'); - } catch (error) { - const recovered = resultFromPostStreamError({ - error, - submitAttempts: submitAttemptHistory, - mainWorkflowPath, - workItemId, - runId: context.runId, - taskId, - }); - if (recovered) { - await promoteMainWorkflow( - domainContext, - context.logger, - recovered.outcome.workflowId, + if (workflowId) { + try { + const json = await domainContext.workflowService.getAsWorkflowJSON(workflowId); + const rawCode = generateWorkflowCode(json); + const code = `${SDK_IMPORT_STATEMENT}\n\n${rawCode}`; + await writeBuilderWorkspaceFile(workspace, builderLayout.mainWorkflowPath, code); + } catch { + // Non-fatal — agent can still build from scratch + } + } else { + await writeBuilderWorkspaceFile( + workspace, + builderLayout.mainWorkflowPath, + `${SDK_IMPORT_STATEMENT}\n\n`, ); - return await finalizeBuildResult(context, workItemId, recovered); } - throw error; - } - const mainWorkflowAttempt = submitAttempts.get(mainWorkflowPath); - const currentMainWorkflow = await readFileViaSandbox(workspace, mainWorkflowPath); - const currentMainWorkflowHash = hashContent(currentMainWorkflow); + const mainWorkflowPath = builderLayout.mainWorkflowPath; + const initialMainWorkflowSnapshot = createMainWorkflowSnapshot( + await readFileViaSandbox(workspace, mainWorkflowPath), + ); + builderTools.set( + 'submit-workflow', + createIdentityEnforcedSubmitWorkflowTool({ + context: domainContext, + workspace, + credentialMap: credMap, + root, + defaultFilePath: mainWorkflowPath, + currentRunId: context.runId, + getWorkflowLoopState: async () => + await context.workflowTaskService?.getWorkflowLoopState(workItemId), + onGuardFired: (event) => { + context.trackTelemetry?.('Builder remediation guard fired', { + thread_id: context.threadId, + run_id: context.runId, + work_item_id: workItemId, + workflow_id: event.workflowId, + category: event.category, + attempt_count: event.attemptCount, + reason: event.reason, + }); + }, + onAttempt: async (attempt) => { + submitAttempts.set(attempt.filePath, attempt); + submitAttemptHistory.push(attempt); + if (attempt.filePath !== mainWorkflowPath) { + return; + } + if (!context.workflowTaskService) { + return; + } - if (!mainWorkflowAttempt) { - return await settleMissingMainWorkflowSubmit({ + await context.workflowTaskService.reportBuildOutcome( + buildOutcome( + workItemId, + context.runId, + taskId, + attempt, + attempt.success + ? 'Workflow submitted and ready for verification.' + : (attempt.errors?.join(' ') ?? 'Workflow submission failed.'), + ), + ); + }, + }), + ); + + const tracedBuilderTools = traceSubAgentTools( context, - workItemId, - runId: context.runId, - taskId, - workflowId, - mainWorkflowPath, - initialMainWorkflowSnapshot, - currentMainWorkflow, - currentMainWorkflowHash, - submitTool: tracedBuilderTools.get('submit-workflow'), - submitAttempts, - submitAttemptHistory, - finalText, - onSuccessfulSubmit: async (attempt) => - await finalizeSuccessfulMainWorkflowSubmit({ - context, - binding: builderMemoryBinding, - domainContext, - workItemId, - taskId, - mainWorkflowPath, - mainWorkflowAttempt: attempt, - submitAttemptHistory, - lastRequestedChange: input.task, - finalText, - shouldUseBuilderMemory, - }), - onRecoveredSubmit: async (recovered) => { + builderTools, + 'workflow-builder', + ); + const runtimeWorkspaceTools = toToolRegistry(workspace.getTools()); + const builderMemory = getBuilderSessionMemory(context, true); + const shouldUseBuilderMemory = Boolean(builderMemory); + + const subAgent = new Agent('Workflow Builder Agent') + .model(context.modelId) + .instructions(prompt, { + providerOptions: { + anthropic: { cacheControl: { type: 'ephemeral' } }, + }, + }) + .tool(toolRegistryValues(tracedBuilderTools)) + .workspace(workspace) + .checkpoint(context.checkpointStore ?? 'memory'); + if (builderMemory) { + subAgent.memory(builderMemory); + } + const telemetry = traceContext?.getTelemetry?.({ + agentRole: 'workflow-builder', + functionId: 'instance-ai.subagent.workflow-builder', + executionMode: 'background_subagent', + metadata: { agent_id: subAgentId, task_id: taskId }, + }); + if (telemetry) { + subAgent.telemetry(telemetry); + } + mergeTraceRunInputs( + traceContext?.actorRun, + buildAgentTraceInputs({ + systemPrompt: prompt, + tools: tracedBuilderTools, + runtimeTools: runtimeWorkspaceTools, + modelId: context.modelId, + }), + ); + + let finalText: string; + try { + const persistence = await createSubAgentPersistence(context, { + agentKind: 'workflow-builder', + threadId: builderThreadId, + resourceId: builderResourceId, + }); + const resumeOptions: Record = { + providerOptions: { + anthropic: { cacheControl: { type: 'ephemeral' } }, + }, + }; + const stream = await subAgent.stream(briefing, { + maxIterations: MAX_STEPS.BUILDER, + abortSignal: signal, + persistence, + providerOptions: { + anthropic: { cacheControl: { type: 'ephemeral' } }, + }, + }); + + const hitlResult = await consumeStreamWithHitl({ + agent: subAgent, + stream, + runId: context.runId, + agentId: subAgentId, + eventBus: context.eventBus, + logger: context.logger, + threadId: context.threadId, + abortSignal: signal, + waitForConfirmation: context.waitForConfirmation, + drainCorrections, + waitForCorrection, + maxIterations: MAX_STEPS.BUILDER, + resumeOptions, + persistence, + }); + + finalText = await requireCompletedHitlText(hitlResult, 'Workflow builder sub-agent'); + } catch (error) { + const recovered = resultFromPostStreamError({ + error, + submitAttempts: submitAttemptHistory, + mainWorkflowPath, + workItemId, + runId: context.runId, + taskId, + }); + if (recovered) { await promoteMainWorkflow( domainContext, context.logger, recovered.outcome.workflowId, ); return await finalizeBuildResult(context, workItemId, recovered); - }, - }); - } - - if (!mainWorkflowAttempt.success) { - const recovered = resultFromLaterFailedMainSubmit({ - failedAttempt: mainWorkflowAttempt, - submitAttempts: submitAttemptHistory, - mainWorkflowPath, - workItemId, - runId: context.runId, - taskId, - }); - if (recovered) { - await promoteMainWorkflow( - domainContext, - context.logger, - recovered.outcome.workflowId, - ); - return await finalizeBuildResult(context, workItemId, recovered); + } + throw error; } - const errorText = - mainWorkflowAttempt.errors?.join(' ') ?? 'Unknown submit-workflow failure.'; - const text = `Error: workflow builder stopped after a failed submit-workflow for ${mainWorkflowPath}. ${errorText}`; - return { - text, - outcome: buildOutcome(workItemId, context.runId, taskId, mainWorkflowAttempt, text), - }; - } + const mainWorkflowAttempt = submitAttempts.get(mainWorkflowPath); + const currentMainWorkflow = await readFileViaSandbox(workspace, mainWorkflowPath); + const currentMainWorkflowHash = hashContent(currentMainWorkflow); - if (mainWorkflowAttempt.sourceHash !== currentMainWorkflowHash) { - // Builder edited the file after its last submit — auto-re-submit - // instead of discarding the agent's work. - const submitTool = tracedBuilderTools.get('submit-workflow'); - if (submitTool?.handler) { - const resubmit = (await submitTool.handler( - { - filePath: mainWorkflowPath, - workflowId: mainWorkflowAttempt.workflowId, - }, - {}, - )) as SubmitWorkflowOutput; - - const refreshedAttempt = attemptFromAutoResubmit({ - latestAttempt: submitAttempts.get(mainWorkflowPath), - resubmit, - filePath: mainWorkflowPath, - sourceHash: currentMainWorkflowHash, - }); - if (resubmit.success && refreshedAttempt?.success) { - await promoteMainWorkflow( - domainContext, - context.logger, - refreshedAttempt.workflowId, - ); - await compactSuccessfulBuilderMemory({ - context, - binding: builderMemoryBinding, - domainContext, - workflowId: refreshedAttempt.workflowId, - workItemId, - mainWorkflowPath, - mainWorkflowAttempt: refreshedAttempt, - lastRequestedChange: input.task, - finalText, - shouldUseBuilderMemory, - }); - const outcome = await buildOutcomeWithLatestVerification( - context, - workItemId, - taskId, - refreshedAttempt, - finalText, - ); - return { - text: finalText, - outcome, - }; - } - - const resubmitErrors = - refreshedAttempt?.errors?.join(' ') ?? - formatSubmitWorkflowErrors(resubmit, 'Auto-re-submit failed.'); - if ( - refreshedAttempt && - !refreshedAttempt.success && - shouldRecoverSavedWorkflowAfterFailedSubmit(refreshedAttempt) - ) { - const recovered = resultFromLaterFailedMainSubmit({ - failedAttempt: refreshedAttempt, - submitAttempts: submitAttemptHistory, - mainWorkflowPath, - workItemId, - runId: context.runId, - taskId, - }); - if (recovered) { + if (!mainWorkflowAttempt) { + return await settleMissingMainWorkflowSubmit({ + context, + workItemId, + runId: context.runId, + taskId, + workflowId, + mainWorkflowPath, + initialMainWorkflowSnapshot, + currentMainWorkflow, + currentMainWorkflowHash, + submitTool: tracedBuilderTools.get('submit-workflow'), + submitAttempts, + submitAttemptHistory, + finalText, + onSuccessfulSubmit: async (attempt) => + await finalizeSuccessfulMainWorkflowSubmit({ + context, + binding: builderMemoryBinding, + domainContext, + workItemId, + taskId, + mainWorkflowPath, + mainWorkflowAttempt: attempt, + submitAttemptHistory, + lastRequestedChange: input.task, + finalText, + shouldUseBuilderMemory, + }), + onRecoveredSubmit: async (recovered) => { await promoteMainWorkflow( domainContext, context.logger, recovered.outcome.workflowId, ); return await finalizeBuildResult(context, workItemId, recovered); - } + }, + }); + } + + if (!mainWorkflowAttempt.success) { + const recovered = resultFromLaterFailedMainSubmit({ + failedAttempt: mainWorkflowAttempt, + submitAttempts: submitAttemptHistory, + mainWorkflowPath, + workItemId, + runId: context.runId, + taskId, + }); + if (recovered) { + await promoteMainWorkflow( + domainContext, + context.logger, + recovered.outcome.workflowId, + ); + return await finalizeBuildResult(context, workItemId, recovered); } - const text = `Error: auto-re-submit of edited ${mainWorkflowPath} failed. ${resubmitErrors}`; + + const errorText = + mainWorkflowAttempt.errors?.join(' ') ?? 'Unknown submit-workflow failure.'; + const text = `Error: workflow builder stopped after a failed submit-workflow for ${mainWorkflowPath}. ${errorText}`; return { text, - outcome: buildOutcome( - workItemId, - context.runId, - taskId, - refreshedAttempt ?? undefined, - text, - ), + outcome: buildOutcome(workItemId, context.runId, taskId, mainWorkflowAttempt, text), }; } - } - await promoteMainWorkflow(domainContext, context.logger, mainWorkflowAttempt.workflowId); - await compactSuccessfulBuilderMemory({ - context, - binding: builderMemoryBinding, - domainContext, - workflowId: mainWorkflowAttempt.workflowId, - workItemId, - mainWorkflowPath, - mainWorkflowAttempt, - lastRequestedChange: input.task, - finalText, - shouldUseBuilderMemory, - }); - const outcome = await buildOutcomeWithLatestVerification( - context, - workItemId, - taskId, - mainWorkflowAttempt, - finalText, - ); - return { - text: finalText, - outcome, - }; + if (mainWorkflowAttempt.sourceHash !== currentMainWorkflowHash) { + // Builder edited the file after its last submit — auto-re-submit + // instead of discarding the agent's work. + const submitTool = tracedBuilderTools.get('submit-workflow'); + if (submitTool?.handler) { + const resubmit = (await submitTool.handler( + { + filePath: mainWorkflowPath, + workflowId: mainWorkflowAttempt.workflowId, + }, + {}, + )) as SubmitWorkflowOutput; + + const refreshedAttempt = attemptFromAutoResubmit({ + latestAttempt: submitAttempts.get(mainWorkflowPath), + resubmit, + filePath: mainWorkflowPath, + sourceHash: currentMainWorkflowHash, + }); + if (resubmit.success && refreshedAttempt?.success) { + await promoteMainWorkflow( + domainContext, + context.logger, + refreshedAttempt.workflowId, + ); + await compactSuccessfulBuilderMemory({ + context, + binding: builderMemoryBinding, + domainContext, + workflowId: refreshedAttempt.workflowId, + workItemId, + mainWorkflowPath, + mainWorkflowAttempt: refreshedAttempt, + lastRequestedChange: input.task, + finalText, + shouldUseBuilderMemory, + }); + const outcome = await buildOutcomeWithLatestVerification( + context, + workItemId, + taskId, + refreshedAttempt, + finalText, + ); + return { + text: finalText, + outcome, + }; + } + + const resubmitErrors = + refreshedAttempt?.errors?.join(' ') ?? + formatSubmitWorkflowErrors(resubmit, 'Auto-re-submit failed.'); + if ( + refreshedAttempt && + !refreshedAttempt.success && + shouldRecoverSavedWorkflowAfterFailedSubmit(refreshedAttempt) + ) { + const recovered = resultFromLaterFailedMainSubmit({ + failedAttempt: refreshedAttempt, + submitAttempts: submitAttemptHistory, + mainWorkflowPath, + workItemId, + runId: context.runId, + taskId, + }); + if (recovered) { + await promoteMainWorkflow( + domainContext, + context.logger, + recovered.outcome.workflowId, + ); + return await finalizeBuildResult(context, workItemId, recovered); + } + } + const text = `Error: auto-re-submit of edited ${mainWorkflowPath} failed. ${resubmitErrors}`; + return { + text, + outcome: buildOutcome( + workItemId, + context.runId, + taskId, + refreshedAttempt ?? undefined, + text, + ), + }; + } + } + + await promoteMainWorkflow( + domainContext, + context.logger, + mainWorkflowAttempt.workflowId, + ); + await compactSuccessfulBuilderMemory({ + context, + binding: builderMemoryBinding, + domainContext, + workflowId: mainWorkflowAttempt.workflowId, + workItemId, + mainWorkflowPath, + mainWorkflowAttempt, + lastRequestedChange: input.task, + finalText, + shouldUseBuilderMemory, + }); + const outcome = await buildOutcomeWithLatestVerification( + context, + workItemId, + taskId, + mainWorkflowAttempt, + finalText, + ); + return { + text: finalText, + outcome, + }; + } finally { + unsubscribeTelemetry?.(); + if (telemetrySession) { + try { + telemetrySession.flush(); + detachTemplateTelemetrySession(workspace); + } catch (error) { + context.logger.warn('build-workflow-agent: failed to flush template telemetry', { + error: error instanceof Error ? error.message : String(error), + }); + } + } + } } let fallbackMainWorkflowId: string | undefined; diff --git a/packages/@n8n/instance-ai/src/types.ts b/packages/@n8n/instance-ai/src/types.ts index 9ffc0cf43f8..a7b11d81c9e 100644 --- a/packages/@n8n/instance-ai/src/types.ts +++ b/packages/@n8n/instance-ai/src/types.ts @@ -40,6 +40,7 @@ import type { WorkflowLoopAction, WorkflowLoopState, } from './workflow-loop/workflow-loop-state'; +import type { BuilderTemplatesService } from './workspace/builder-templates-service'; // ── Data shapes ────────────────────────────────────────────────────────────── @@ -611,6 +612,12 @@ export interface InstanceAiContext { nodeService: InstanceAiNodeService; dataTableService: InstanceAiDataTableService; webResearchService?: InstanceAiWebResearchService; + /** + * Curated workflow-template provider for the sandbox setup. When absent or + * when the service returns an empty bundle, the sandbox is created without + * an `examples/` directory and the agent operates without template hints. + */ + templatesService?: BuilderTemplatesService; workspaceService?: InstanceAiWorkspaceService; /** * Connected remote MCP server (e.g. computer-use daemon). When set, dynamic tools are created from its advertised capabilities. diff --git a/packages/@n8n/instance-ai/src/workspace/__tests__/builder-templates-service.test.ts b/packages/@n8n/instance-ai/src/workspace/__tests__/builder-templates-service.test.ts new file mode 100644 index 00000000000..7d20b9c8601 --- /dev/null +++ b/packages/@n8n/instance-ai/src/workspace/__tests__/builder-templates-service.test.ts @@ -0,0 +1,647 @@ +import { createHash } from 'node:crypto'; +import * as fsp from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { + BuilderTemplatesService, + type BuilderTemplatesServiceOptions, + builderTemplatesOptionsFromEnv, +} from '../builder-templates-service'; + +const ORIGINAL_FETCH = globalThis.fetch; + +function sha256Hex(buf: Buffer): string { + return createHash('sha256').update(buf).digest('hex'); +} + +function archiveResponse(buffer: Buffer, etag: string | null, status = 200): Response { + const headers: Record = { 'content-type': 'application/gzip' }; + if (etag) headers.etag = etag; + return new Response(new Uint8Array(buffer), { status, headers }); +} + +interface MockState { + /** Opaque archive bytes — the service treats these as a black box, no extraction. */ + archive: Buffer; + etag: string | null; + /** Default status for an archive fetch (used by both channels when not overridden). */ + archiveStatus?: number; + /** Per-channel status override for the `/v/` URL. */ + exactStatus?: number; + /** Per-channel status override for the `/latest/` URL. */ + latestStatus?: number; + respondNotModified?: boolean; + /** When `null`, sidecar returns 404; when a string, that body is served; default = correct sha. */ + sha256Override?: string | null; + /** Force the first N archive requests to return 503; subsequent requests behave normally. */ + transientFailuresBeforeSuccess?: number; + /** When true, the archive 200 response omits its ETag header. */ + omitEtagHeader?: boolean; + calls: { + fetch: number; + archiveFetches: number; + exactFetches: number; + latestFetches: number; + lastIfNoneMatch: string | null; + }; +} + +function isExactArchiveUrl(url: string): boolean { + return /\/v\d+\.\d+\/templates\.tar\.gz$/.test(url); +} + +function isLatestArchiveUrl(url: string): boolean { + return url.endsWith('/latest/templates.tar.gz'); +} + +function installMockFetch(state: MockState): jest.Mock { + const mock = jest.fn((input: string | URL | Request, init?: RequestInit) => { + state.calls.fetch++; + const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url; + const headers = new Headers((init?.headers ?? {}) as Record); + + if (url.endsWith('/templates.tar.gz.sha256')) { + if (state.sha256Override === null) return new Response('', { status: 404 }); + const body = state.sha256Override ?? sha256Hex(state.archive); + return new Response(body, { + status: 200, + headers: { 'content-type': 'text/plain' }, + }); + } + + const exact = isExactArchiveUrl(url); + const latest = isLatestArchiveUrl(url); + if (!exact && !latest) { + return new Response('unhandled', { status: 500 }); + } + + state.calls.archiveFetches++; + if (exact) state.calls.exactFetches++; + else state.calls.latestFetches++; + state.calls.lastIfNoneMatch = headers.get('if-none-match'); + + if (state.respondNotModified) { + return new Response(null, { status: 304 }); + } + + if (state.transientFailuresBeforeSuccess && state.transientFailuresBeforeSuccess > 0) { + state.transientFailuresBeforeSuccess--; + return new Response('temporarily unavailable', { status: 503 }); + } + + const channelStatus = exact ? state.exactStatus : state.latestStatus; + const status = channelStatus ?? state.archiveStatus ?? 200; + if (status >= 400) return new Response('error', { status }); + const etag = state.omitEtagHeader ? null : state.etag; + return archiveResponse(state.archive, etag, status); + }); + globalThis.fetch = mock as unknown as typeof globalThis.fetch; + return mock; +} + +async function makeTempDir(): Promise { + return await fsp.mkdtemp(path.join(os.tmpdir(), 'builder-templates-svc-')); +} + +function makeOptions( + cacheDir: string, + overrides: Partial = {}, +): BuilderTemplatesServiceOptions { + return { + cdnBaseUrl: 'https://cdn.example/n8n-sdk-templates', + sdkVersion: '0.15.0', + cacheDir, + refreshIntervalMs: 60_000, + fetchTimeoutMs: 1_000, + // Keep retry tests fast; production default is much higher. + retryBackoffBaseMs: 1, + ...overrides, + }; +} + +function makeState(): MockState { + return { + archive: Buffer.from('opaque-archive-bytes-v1'), + etag: '"sha-1"', + calls: { + fetch: 0, + archiveFetches: 0, + exactFetches: 0, + latestFetches: 0, + lastIfNoneMatch: null, + }, + }; +} + +describe('BuilderTemplatesService', () => { + afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + }); + + it('fetches templates.tar.gz on first call and populates disk cache', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir)); + const bundle = await svc.getBundle(); + + expect(bundle.archive?.equals(state.archive)).toBe(true); + expect(bundle.version).toBe('"sha-1"'); + + const cachedArchive = await fsp.readFile(path.join(cacheDir, 'templates.tar.gz')); + expect(cachedArchive.equals(state.archive)).toBe(true); + const cachedEtag = await fsp.readFile(path.join(cacheDir, 'etag.txt'), 'utf-8'); + expect(cachedEtag).toBe('"sha-1"'); + const cachedSha = await fsp.readFile(path.join(cacheDir, 'templates.tar.gz.sha256'), 'utf-8'); + expect(cachedSha).toBe(sha256Hex(state.archive)); + }); + + it('returns an empty bundle when the fetch fails and there is no disk cache', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + state.archiveStatus = 500; + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir)); + const bundle = await svc.getBundle(); + + expect(bundle.archive).toBeNull(); + expect(bundle.version).toBeNull(); + }); + + it('does not retry a failed cold-start hydrate inside the failure cooldown', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + state.archiveStatus = 500; + installMockFetch(state); + + const svc = new BuilderTemplatesService( + makeOptions(cacheDir, { failureRetryIntervalMs: 60_000, maxAttempts: 1 }), + ); + const failedBundle = await svc.getBundle(); + state.archiveStatus = 200; + const skippedBundle = await svc.getBundle(); + + expect(failedBundle.archive).toBeNull(); + expect(skippedBundle.archive).toBeNull(); + expect(state.calls.archiveFetches).toBe(1); + }); + + it('retries a failed cold-start hydrate after the failure cooldown', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + state.archiveStatus = 500; + installMockFetch(state); + const dateNow = jest.spyOn(Date, 'now'); + dateNow.mockReturnValue(1_000); + + try { + const svc = new BuilderTemplatesService( + makeOptions(cacheDir, { failureRetryIntervalMs: 100, maxAttempts: 1 }), + ); + await svc.getBundle(); + state.archiveStatus = 200; + + dateNow.mockReturnValue(1_050); + const skippedBundle = await svc.getBundle(); + dateNow.mockReturnValue(1_101); + const retriedBundle = await svc.getBundle(); + + expect(skippedBundle.archive).toBeNull(); + expect(retriedBundle.archive?.equals(state.archive)).toBe(true); + expect(state.calls.archiveFetches).toBe(2); + } finally { + dateNow.mockRestore(); + } + }); + + it('memoises subsequent calls and does not refetch when cache is fresh', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir, { refreshIntervalMs: 60_000 })); + await svc.getBundle(); + const callsAfterFirst = state.calls.fetch; + await svc.getBundle(); + expect(state.calls.fetch).toBe(callsAfterFirst); + }); + + it('sends If-None-Match on refresh and short-circuits on 304', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + installMockFetch(state); + + const seedSvc = new BuilderTemplatesService(makeOptions(cacheDir)); + await seedSvc.getBundle(); + + // Backdate the cache so the TTL window expires immediately. + await fsp.utimes(path.join(cacheDir, 'templates.tar.gz'), 0, 0); + state.respondNotModified = true; + + const svc = new BuilderTemplatesService(makeOptions(cacheDir, { refreshIntervalMs: 1 })); + const bundle = await svc.getBundle(); + // Background refresh is fire-and-forget; let it run. + await new Promise((r) => setTimeout(r, 20)); + + expect(bundle.version).toBe('"sha-1"'); + expect(state.calls.lastIfNoneMatch).toBe('"sha-1"'); + }); + + it('short-circuits to an empty bundle when disabled', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + const fetchMock = installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir, { disabled: true })); + const bundle = await svc.getBundle(); + + expect(bundle.archive).toBeNull(); + expect(bundle.version).toBeNull(); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('hydrates from disk and reports the cached version', async () => { + const cacheDir = await makeTempDir(); + await fsp.mkdir(cacheDir, { recursive: true }); + const archive = Buffer.from('opaque-archive-bytes-pre-existing'); + await fsp.writeFile(path.join(cacheDir, 'templates.tar.gz'), archive); + await fsp.writeFile(path.join(cacheDir, 'etag.txt'), '"pre-existing"'); + await fsp.writeFile(path.join(cacheDir, 'channel.txt'), 'exact'); + + // Block any network call so we know hydration came from disk. + globalThis.fetch = jest.fn( + () => new Response('', { status: 500 }), + ) as unknown as typeof globalThis.fetch; + + const svc = new BuilderTemplatesService(makeOptions(cacheDir, { refreshIntervalMs: 60_000 })); + const bundle = await svc.getBundle(); + + expect(bundle.version).toBe('"pre-existing"'); + expect(bundle.archive?.equals(archive)).toBe(true); + // getVersion() prefixes with the channel + strips quotes for telemetry use; raw etag stays on bundle.version. + expect(svc.getVersion()).toBe('v0.15:pre-existing'); + }); + + it('keeps the existing bundle when the refresh fetch errors', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + installMockFetch(state); + + const seedSvc = new BuilderTemplatesService(makeOptions(cacheDir)); + await seedSvc.getBundle(); + + state.archiveStatus = 503; + await fsp.utimes(path.join(cacheDir, 'templates.tar.gz'), 0, 0); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir, { refreshIntervalMs: 1 })); + const bundle = await svc.getBundle(); + await new Promise((r) => setTimeout(r, 20)); + + expect(bundle.version).toBe('"sha-1"'); + expect(bundle.archive?.equals(state.archive)).toBe(true); + }); + + it('does not send If-None-Match on initial fetch when only an orphan etag exists on disk', async () => { + // Simulate a previously-cached etag without a matching archive — e.g. the + // archive was deleted or never finished writing. A 304 here would leave the + // service permanently empty for the process, so the initial request must + // be unconditional. + const cacheDir = await makeTempDir(); + await fsp.mkdir(cacheDir, { recursive: true }); + await fsp.writeFile(path.join(cacheDir, 'etag.txt'), '"orphan"'); + + const state = makeState(); + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir)); + const bundle = await svc.getBundle(); + + expect(state.calls.lastIfNoneMatch).toBeNull(); + expect(bundle.version).toBe('"sha-1"'); + expect(bundle.archive?.equals(state.archive)).toBe(true); + }); + + it('unlinks etag.txt when refresh returns 200 without an ETag header', async () => { + const cacheDir = await makeTempDir(); + await fsp.mkdir(cacheDir, { recursive: true }); + await fsp.writeFile(path.join(cacheDir, 'etag.txt'), '"stale"'); + + const state = makeState(); + state.omitEtagHeader = true; + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir)); + const bundle = await svc.getBundle(); + + expect(bundle.version).toBeNull(); + await expect(fsp.stat(path.join(cacheDir, 'etag.txt'))).rejects.toMatchObject({ + code: 'ENOENT', + }); + }); + + it('persists etag.txt before templates.tar.gz (crash-safety)', async () => { + // Pre-create templates.tar.gz as a non-empty directory so the atomic + // rename for the archive step fails. With etag-first ordering, etag.txt + // should already be on disk when the archive write blows up. + const cacheDir = await makeTempDir(); + const blockedArchivePath = path.join(cacheDir, 'templates.tar.gz'); + await fsp.mkdir(blockedArchivePath); + await fsp.writeFile(path.join(blockedArchivePath, 'block'), ''); + + const state = makeState(); + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir)); + await svc.getBundle(); + + const etagOnDisk = await fsp.readFile(path.join(cacheDir, 'etag.txt'), 'utf-8'); + expect(etagOnDisk).toBe('"sha-1"'); + // The pre-existing directory is untouched — rename never succeeded. + expect((await fsp.stat(blockedArchivePath)).isDirectory()).toBe(true); + }); + + it('retries on transient 5xx during cold-start hydrate', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + state.transientFailuresBeforeSuccess = 2; + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir, { maxAttempts: 3 })); + const bundle = await svc.getBundle(); + + expect(bundle.archive?.equals(state.archive)).toBe(true); + expect(state.calls.archiveFetches).toBe(3); + }); + + it('does not retry on 4xx', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + state.archiveStatus = 403; + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir, { maxAttempts: 3 })); + const bundle = await svc.getBundle(); + + expect(bundle.archive).toBeNull(); + expect(state.calls.archiveFetches).toBe(1); + }); + + it('rejects the downloaded bundle when sha256 sidecar mismatches', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + state.sha256Override = 'deadbeef'.repeat(8); // 64 hex chars, but wrong + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir)); + const bundle = await svc.getBundle(); + + expect(bundle.archive).toBeNull(); + expect(bundle.version).toBeNull(); + // Cache must not be written on integrity failure + await expect(fsp.stat(path.join(cacheDir, 'templates.tar.gz'))).rejects.toMatchObject({ + code: 'ENOENT', + }); + }); + + it('accepts the bundle when sha256 sidecar matches', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + // sha256Override undefined → mock serves the correct digest + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir)); + const bundle = await svc.getBundle(); + + expect(bundle.archive?.equals(state.archive)).toBe(true); + expect(bundle.version).toBe('"sha-1"'); + }); + + it('accepts the bundle when the sha256 sidecar 404s (defence-in-depth, not required)', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + state.sha256Override = null; // sidecar 404 + const logger = { warn: jest.fn(), info: jest.fn(), error: jest.fn(), debug: jest.fn() }; + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir, { logger })); + const bundle = await svc.getBundle(); + + expect(bundle.archive?.equals(state.archive)).toBe(true); + expect(bundle.version).toBe('"sha-1"'); + }); + + it('drops the disk cache when a persisted sha256 does not match the on-disk archive', async () => { + const cacheDir = await makeTempDir(); + await fsp.mkdir(cacheDir, { recursive: true }); + const corruptArchive = Buffer.from('this-is-the-corrupt-archive-on-disk'); + await fsp.writeFile(path.join(cacheDir, 'templates.tar.gz'), corruptArchive); + await fsp.writeFile(path.join(cacheDir, 'etag.txt'), '"stale"'); + await fsp.writeFile(path.join(cacheDir, 'channel.txt'), 'exact'); + // Sha that does NOT match the archive on disk + await fsp.writeFile(path.join(cacheDir, 'templates.tar.gz.sha256'), 'deadbeef'.repeat(8)); + + // Live CDN serves a different bundle → service should refetch on mismatch + const state = makeState(); + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir)); + const bundle = await svc.getBundle(); + + // Came from the network, not the corrupt disk cache + expect(bundle.version).toBe('"sha-1"'); + expect(bundle.archive?.equals(state.archive)).toBe(true); + // Initial network fetch must not echo the disk's stale etag + expect(state.calls.lastIfNoneMatch).toBeNull(); + }); + + describe('versioned URLs', () => { + it('fetches /v./templates.tar.gz when SDK version is set', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + const fetchMock = installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir, { sdkVersion: '0.15.0' })); + const bundle = await svc.getBundle(); + + expect(bundle.archive?.equals(state.archive)).toBe(true); + expect(fetchMock).toHaveBeenCalledWith( + 'https://cdn.example/n8n-sdk-templates/v0.15/templates.tar.gz', + expect.any(Object), + ); + }); + + it('prefixes getVersion with the exact channel (v.:)', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir, { sdkVersion: '0.15.0' })); + await svc.getBundle(); + expect(svc.getVersion()).toBe('v0.15:sha-1'); + }); + + it('returns an empty bundle when both /v/ and /latest/ return 404', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + state.exactStatus = 404; + state.latestStatus = 404; + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir, { sdkVersion: '0.17.0' })); + const bundle = await svc.getBundle(); + + expect(bundle.archive).toBeNull(); + expect(bundle.version).toBeNull(); + expect(state.calls.exactFetches).toBe(1); + expect(state.calls.latestFetches).toBe(1); + }); + + it('does not fall back to /latest/ when the exact channel returns 500 (transport error)', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + state.exactStatus = 500; + installMockFetch(state); + + const svc = new BuilderTemplatesService( + makeOptions(cacheDir, { sdkVersion: '0.15.0', maxAttempts: 1 }), + ); + const bundle = await svc.getBundle(); + + expect(bundle.archive).toBeNull(); + expect(state.calls.latestFetches).toBe(0); + }); + + it('drops legacy disk cache when channel.txt is missing and refetches fresh', async () => { + const cacheDir = await makeTempDir(); + await fsp.mkdir(cacheDir, { recursive: true }); + const legacyArchive = Buffer.from('opaque-archive-legacy'); + await fsp.writeFile(path.join(cacheDir, 'templates.tar.gz'), legacyArchive); + await fsp.writeFile(path.join(cacheDir, 'etag.txt'), '"legacy-etag"'); + await fsp.writeFile(path.join(cacheDir, 'templates.tar.gz.sha256'), sha256Hex(legacyArchive)); + // Note: no channel.txt — represents a pre-versioned cache layout. + + const state = makeState(); + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir, { sdkVersion: '0.15.0' })); + const bundle = await svc.getBundle(); + + // Came from the network, not the legacy disk archive. + expect(bundle.archive?.equals(state.archive)).toBe(true); + expect(svc.getVersion()).toBe('v0.15:sha-1'); + expect(state.calls.lastIfNoneMatch).toBeNull(); + + const channelOnDisk = await fsp.readFile(path.join(cacheDir, 'channel.txt'), 'utf-8'); + expect(channelOnDisk).toBe('exact'); + }); + + it('honours channel.txt on warm restart so getVersion keeps the latest: prefix', async () => { + const cacheDir = await makeTempDir(); + await fsp.mkdir(cacheDir, { recursive: true }); + const archive = Buffer.from('opaque-archive-bytes-from-latest'); + await fsp.writeFile(path.join(cacheDir, 'templates.tar.gz'), archive); + await fsp.writeFile(path.join(cacheDir, 'etag.txt'), '"latest-etag"'); + await fsp.writeFile(path.join(cacheDir, 'templates.tar.gz.sha256'), sha256Hex(archive)); + await fsp.writeFile(path.join(cacheDir, 'channel.txt'), 'latest'); + + globalThis.fetch = jest.fn( + () => new Response('', { status: 500 }), + ) as unknown as typeof globalThis.fetch; + + const svc = new BuilderTemplatesService( + makeOptions(cacheDir, { sdkVersion: '0.17.0', refreshIntervalMs: 60_000 }), + ); + const bundle = await svc.getBundle(); + + expect(bundle.archive?.equals(archive)).toBe(true); + expect(svc.getVersion()).toBe('latest:latest-etag'); + }); + + it('falls back to /latest/ when /v/ returns 404', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + state.exactStatus = 404; + const logger = { warn: jest.fn(), info: jest.fn(), error: jest.fn(), debug: jest.fn() }; + installMockFetch(state); + + const svc = new BuilderTemplatesService( + makeOptions(cacheDir, { sdkVersion: '0.17.0', logger }), + ); + const bundle = await svc.getBundle(); + + expect(bundle.archive?.equals(state.archive)).toBe(true); + expect(svc.getVersion()).toBe('latest:sha-1'); + expect(state.calls.exactFetches).toBeGreaterThan(0); + expect(state.calls.latestFetches).toBeGreaterThan(0); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('falling back to /latest/'), + expect.any(Object), + ); + }); + }); + + it('getVersion strips the W/ prefix and surrounding quotes for telemetry', async () => { + const cacheDir = await makeTempDir(); + const state = makeState(); + state.etag = 'W/"abc-123"'; + installMockFetch(state); + + const svc = new BuilderTemplatesService(makeOptions(cacheDir)); + await svc.getBundle(); + expect(svc.getVersion()).toBe('v0.15:abc-123'); + }); +}); + +describe('builderTemplatesOptionsFromEnv', () => { + const ORIGINAL_ENV = { ...process.env }; + + afterEach(() => { + process.env = { ...ORIGINAL_ENV }; + }); + + function clearEnv() { + delete process.env.N8N_INSTANCE_AI_TEMPLATES_URL; + delete process.env.N8N_INSTANCE_AI_TEMPLATES_REFRESH_HOURS; + delete process.env.N8N_INSTANCE_AI_TEMPLATES_DISABLED; + } + + it('parses a valid refresh hours value', () => { + clearEnv(); + process.env.N8N_INSTANCE_AI_TEMPLATES_REFRESH_HOURS = '6'; + const opts = builderTemplatesOptionsFromEnv(); + expect(opts.refreshIntervalMs).toBe(6 * 60 * 60 * 1000); + }); + + it('omits refreshIntervalMs and warns when refresh hours is not a number', () => { + clearEnv(); + process.env.N8N_INSTANCE_AI_TEMPLATES_REFRESH_HOURS = 'banana'; + const logger = { warn: jest.fn(), info: jest.fn(), error: jest.fn(), debug: jest.fn() }; + const opts = builderTemplatesOptionsFromEnv({ logger }); + expect(opts.refreshIntervalMs).toBeUndefined(); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('N8N_INSTANCE_AI_TEMPLATES_REFRESH_HOURS'), + expect.objectContaining({ value: 'banana' }), + ); + }); + + it('omits refreshIntervalMs when refresh hours is zero or negative', () => { + clearEnv(); + process.env.N8N_INSTANCE_AI_TEMPLATES_REFRESH_HOURS = '0'; + expect(builderTemplatesOptionsFromEnv().refreshIntervalMs).toBeUndefined(); + + process.env.N8N_INSTANCE_AI_TEMPLATES_REFRESH_HOURS = '-4'; + expect(builderTemplatesOptionsFromEnv().refreshIntervalMs).toBeUndefined(); + }); + + it('honours the disabled flag and base URL', () => { + clearEnv(); + process.env.N8N_INSTANCE_AI_TEMPLATES_DISABLED = 'true'; + process.env.N8N_INSTANCE_AI_TEMPLATES_URL = 'https://example.com/v2'; + const opts = builderTemplatesOptionsFromEnv(); + expect(opts.disabled).toBe(true); + expect(opts.cdnBaseUrl).toBe('https://example.com/v2'); + }); +}); diff --git a/packages/@n8n/instance-ai/src/workspace/__tests__/sandbox-fs.test.ts b/packages/@n8n/instance-ai/src/workspace/__tests__/sandbox-fs.test.ts index d8405b0224b..07d174a5fb2 100644 --- a/packages/@n8n/instance-ai/src/workspace/__tests__/sandbox-fs.test.ts +++ b/packages/@n8n/instance-ai/src/workspace/__tests__/sandbox-fs.test.ts @@ -173,6 +173,26 @@ describe('writeFileViaSandbox', () => { expect(commands.every((command) => command.length < 40_000)).toBe(true); expect(commands.some((command) => command.includes('| base64 -d >'))).toBe(false); }); + + it('does not assign to the read-only zsh builtin `status` when capturing exit code', async () => { + const executeCommand = jest.fn().mockResolvedValue({ + exitCode: 0, + stdout: '', + stderr: '', + }); + const workspace = createMockWorkspace({ executeCommand }); + + await writeFileViaSandbox(workspace, '/home/user/test.ts', 'hello'); + + const commands = (executeCommand.mock.calls as Array<[string, ...unknown[]]>).map( + ([command]) => command, + ); + // `status` is read-only in zsh; assigning to it silently drops the + // captured exit code. Confirm the decode command uses a different name. + const decodeCommands = commands.filter((command) => command.includes('base64 -d')); + expect(decodeCommands.length).toBeGreaterThan(0); + expect(decodeCommands.every((command) => !/\bstatus=\$\?/.test(command))).toBe(true); + }); }); describe('readFileViaSandbox', () => { diff --git a/packages/@n8n/instance-ai/src/workspace/__tests__/sandbox-setup.test.ts b/packages/@n8n/instance-ai/src/workspace/__tests__/sandbox-setup.test.ts index 8f6487e5642..9998afa128d 100644 --- a/packages/@n8n/instance-ai/src/workspace/__tests__/sandbox-setup.test.ts +++ b/packages/@n8n/instance-ai/src/workspace/__tests__/sandbox-setup.test.ts @@ -1,6 +1,8 @@ import { jsonParse } from 'n8n-workflow'; +import { gzipSync } from 'node:zlib'; import type { InstanceAiContext, SearchableNodeDescription } from '../../types'; +import type { BuilderTemplatesBundle } from '../builder-templates-service'; import type { SandboxWorkspace } from '../sandbox-fs'; import type { setupSandboxWorkspace as setupSandboxWorkspaceFunction } from '../sandbox-setup'; import { formatNodeCatalogLine, getWorkspaceRoot } from '../sandbox-setup'; @@ -17,7 +19,9 @@ type RunInSandboxMock = jest.Mock< >; type ReadFileViaSandboxMock = jest.Mock, [SandboxWorkspace, string]>; -function createSetupContext(): InstanceAiContext { +function createSetupContext( + templatesBundle: BuilderTemplatesBundle | null = null, +): InstanceAiContext { return { nodeService: { listSearchable: jest.fn().mockResolvedValue([]), @@ -26,6 +30,14 @@ function createSetupContext(): InstanceAiContext { list: jest.fn().mockResolvedValue([]), get: jest.fn(), }, + ...(templatesBundle + ? { + templatesService: { + getBundle: jest.fn().mockResolvedValue(templatesBundle), + getVersion: jest.fn().mockReturnValue(templatesBundle.version), + }, + } + : {}), } as unknown as InstanceAiContext; } @@ -215,7 +227,10 @@ describe('setupSandboxWorkspace', () => { ); }); - it('writes the curated examples bundle into examples/', async () => { + it('never writes examples/ on the local provider even when a bundle is available', async () => { + // Local provider is for SDK dev iteration; the agent operates fine without + // the curated reference set, so setupSandboxWorkspace must not pay the + // per-file/archive write cost here. const runInSandbox: RunInSandboxMock = jest.fn< Promise<{ exitCode: number; stdout: string; stderr: string }>, [SandboxWorkspace, string, string?] @@ -234,11 +249,18 @@ describe('setupSandboxWorkspace', () => { async () => {}, ); - await setupSandboxWorkspace(createLocalWorkspace(writeFile), createSetupContext()); + const bundle: BuilderTemplatesBundle = { + archive: Buffer.from('opaque-archive-bytes'), + version: 'test-sha', + }; + await setupSandboxWorkspace(createLocalWorkspace(writeFile), createSetupContext(bundle)); const writtenPaths = writeFile.mock.calls.map(([path]) => path); - expect(writtenPaths).toContain('/sandbox/examples/index.txt'); - expect(writtenPaths.some((p) => /^\/sandbox\/examples\/.+\.ts$/.test(p))).toBe(true); + expect(writtenPaths.some((p) => p.includes('/examples/'))).toBe(false); + expect(writtenPaths.some((p) => p.endsWith('.templates.tar.gz'))).toBe(false); + // `tar` must not be exec'd on the local provider either. + const tarInvocations = runInSandbox.mock.calls.filter(([, cmd]) => cmd.includes('tar -xzf')); + expect(tarInvocations).toEqual([]); }); it('rejects setup file paths that escape the workspace root', async () => { @@ -456,6 +478,256 @@ describe('getWorkspaceRoot', () => { }); }); +describe('writeCuratedExamples', () => { + afterEach(() => { + jest.dontMock('../sandbox-fs'); + jest.resetModules(); + }); + + type WriteCuratedExamples = ( + workspace: SandboxWorkspace, + bundle: BuilderTemplatesBundle | null, + logger?: { debug?: jest.Mock; warn?: jest.Mock }, + ) => Promise; + + type FsMocks = { + runInSandbox: RunInSandboxMock; + writeFileViaSandbox: jest.Mock, [SandboxWorkspace, string, string | Buffer]>; + }; + + function loadWriteCuratedExamples(): { fn: WriteCuratedExamples; fs: FsMocks } { + const runInSandbox: RunInSandboxMock = jest.fn< + Promise<{ exitCode: number; stdout: string; stderr: string }>, + [SandboxWorkspace, string, string?] + >(); + runInSandbox.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' }); + const writeFileViaSandbox = jest.fn, [SandboxWorkspace, string, string | Buffer]>( + async () => {}, + ); + jest.resetModules(); + jest.doMock('../sandbox-fs', () => ({ + runInSandbox, + readFileViaSandbox: jest.fn().mockResolvedValue(null), + writeFileViaSandbox, + escapeSingleQuotes: (value: string) => value.replace(/'/g, "'\\''"), + })); + + let loaded: { writeCuratedExamples: WriteCuratedExamples } | undefined; + jest.isolateModules(() => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + loaded = require('../sandbox-setup') as { + writeCuratedExamples: WriteCuratedExamples; + }; + }); + if (!loaded) throw new Error('Failed to load sandbox-setup'); + return { fn: loaded.writeCuratedExamples, fs: { runInSandbox, writeFileViaSandbox } }; + } + + function makeDaytonaWorkspace() { + const filesystem = { + provider: 'daytona' as const, + writeFile: jest.fn, [string, Buffer, { recursive?: boolean }?]>(async () => {}), + mkdir: jest.fn, [string, { recursive?: boolean }?]>(async () => {}), + }; + const workspace = { filesystem } as unknown as SandboxWorkspace; + return { workspace, filesystem }; + } + + function makeShellOnlyWorkspace(): SandboxWorkspace { + // No filesystem property → forces the writeFileViaSandbox fallback. + return {} as unknown as SandboxWorkspace; + } + + type TarEntry = { + name: string; + content?: string; + typeFlag?: string; + linkName?: string; + }; + + function makeTarGz(entries: TarEntry[]): Buffer { + const blocks: Buffer[] = []; + for (const entry of entries) { + const content = Buffer.from(entry.content ?? '', 'utf-8'); + const typeFlag = entry.typeFlag ?? '0'; + const size = typeFlag === '0' ? content.byteLength : 0; + const header = Buffer.alloc(512); + + header.write(entry.name, 0, 100, 'utf-8'); + writeTarOctal(header, 100, 8, 0o644); + writeTarOctal(header, 108, 8, 0); + writeTarOctal(header, 116, 8, 0); + writeTarOctal(header, 124, 12, size); + writeTarOctal(header, 136, 12, 0); + header.fill(0x20, 148, 156); + header.write(typeFlag, 156, 1, 'ascii'); + if (entry.linkName) header.write(entry.linkName, 157, 100, 'utf-8'); + header.write('ustar', 257, 5, 'ascii'); + header.write('00', 263, 2, 'ascii'); + + const checksum = header.reduce((sum, byte) => sum + byte, 0); + writeTarChecksum(header, checksum); + blocks.push(header); + + if (size > 0) { + blocks.push(content); + const padding = (512 - (size % 512)) % 512; + if (padding > 0) blocks.push(Buffer.alloc(padding)); + } + } + blocks.push(Buffer.alloc(1024)); + return gzipSync(Buffer.concat(blocks)); + } + + function writeTarOctal(buffer: Buffer, offset: number, length: number, value: number): void { + const octal = value + .toString(8) + .padStart(length - 1, '0') + .slice(-(length - 1)); + buffer.write(octal, offset, length - 1, 'ascii'); + buffer[offset + length - 1] = 0; + } + + function writeTarChecksum(buffer: Buffer, checksum: number): void { + const octal = checksum.toString(8).padStart(6, '0').slice(-6); + buffer.write(octal, 148, 6, 'ascii'); + buffer[154] = 0; + buffer[155] = 0x20; + } + + const ARCHIVE = makeTarGz([ + { name: 'index.txt', content: 'slack-daily-summary.ts | Daily Slack' }, + { name: 'slack-daily-summary.ts', content: 'export default {};' }, + ]); + + it('writes the archive and runs tar on a non-local provider', async () => { + const { fn, fs } = loadWriteCuratedExamples(); + const { workspace, filesystem } = makeDaytonaWorkspace(); + + await fn(workspace, { archive: ARCHIVE, version: '"v1"' }); + + // Filesystem path: mkdir for examples/, then writeFile for the archive. + expect(filesystem.mkdir).toHaveBeenCalledWith(expect.stringContaining('/examples'), { + recursive: true, + }); + expect(filesystem.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/\.templates\.tar\.gz$/), + ARCHIVE, + { recursive: true }, + ); + + // tar exec runs exactly once with extract + rm in one shell expression. + const tarCalls = fs.runInSandbox.mock.calls.filter(([, cmd]) => cmd.includes('tar -xzf')); + expect(tarCalls).toHaveLength(1); + expect(tarCalls[0][1]).toMatch(/tar -xzf .* -C .* rm -f .*/); + // `status` is a read-only builtin in zsh — assigning to it would + // silently drop tar's exit code. Use any other name. + expect(tarCalls[0][1]).not.toMatch(/\bstatus=\$\?/); + }); + + it('falls back to shell writes when the workspace has no filesystem', async () => { + const { fn, fs } = loadWriteCuratedExamples(); + const workspace = makeShellOnlyWorkspace(); + + await fn(workspace, { archive: ARCHIVE, version: '"v1"' }); + + // mkdir is exec'd, then archive written via writeFileViaSandbox, then tar. + const mkdirCalls = fs.runInSandbox.mock.calls.filter(([, cmd]) => cmd.startsWith('mkdir -p')); + expect(mkdirCalls).toHaveLength(1); + + expect(fs.writeFileViaSandbox).toHaveBeenCalledWith( + workspace, + expect.stringMatching(/\.templates\.tar\.gz$/), + ARCHIVE, + ); + + const tarCalls = fs.runInSandbox.mock.calls.filter(([, cmd]) => cmd.includes('tar -xzf')); + expect(tarCalls).toHaveLength(1); + }); + + it('warns and continues when tar exits non-zero', async () => { + const { fn, fs } = loadWriteCuratedExamples(); + fs.runInSandbox.mockImplementation(async (_, cmd) => { + const stderr = cmd.includes('tar -xzf') ? 'tar: bad archive' : ''; + const exitCode = cmd.includes('tar -xzf') ? 1 : 0; + return await Promise.resolve({ exitCode, stdout: '', stderr }); + }); + const { workspace } = makeDaytonaWorkspace(); + const logger = { debug: jest.fn(), warn: jest.fn() }; + + // Must not throw. + await fn(workspace, { archive: ARCHIVE, version: '"v1"' }, logger); + + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('failed to extract'), + expect.objectContaining({ stderr: 'tar: bad archive' }), + ); + }); + + it.each<[string, Buffer]>([ + ['absolute path', makeTarGz([{ name: '/escape.ts', content: 'x' }])], + ['parent traversal', makeTarGz([{ name: '../escape.ts', content: 'x' }])], + ['nested path', makeTarGz([{ name: 'nested/template.ts', content: 'x' }])], + ['symlink entry', makeTarGz([{ name: 'link.ts', typeFlag: '2', linkName: 'target.ts' }])], + ['hardlink entry', makeTarGz([{ name: 'link.ts', typeFlag: '1', linkName: 'target.ts' }])], + ['malformed gzip', Buffer.from('not-a-gzip-archive')], + ])('rejects an archive with %s before writing it', async (_label, archive) => { + const { fn, fs } = loadWriteCuratedExamples(); + const { workspace, filesystem } = makeDaytonaWorkspace(); + const logger = { debug: jest.fn(), warn: jest.fn() }; + + await fn(workspace, { archive, version: '"v1"' }, logger); + + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('rejected curated examples archive'), + expect.objectContaining({ archiveVersion: '"v1"' }), + ); + expect(filesystem.mkdir).not.toHaveBeenCalled(); + expect(filesystem.writeFile).not.toHaveBeenCalled(); + expect(fs.runInSandbox).not.toHaveBeenCalled(); + }); + + it('no-ops when bundle.archive is null', async () => { + const { fn, fs } = loadWriteCuratedExamples(); + const { workspace, filesystem } = makeDaytonaWorkspace(); + + await fn(workspace, { archive: null, version: null }); + + expect(filesystem.writeFile).not.toHaveBeenCalled(); + expect(fs.runInSandbox).not.toHaveBeenCalled(); + }); + + it('no-ops when bundle is null', async () => { + const { fn, fs } = loadWriteCuratedExamples(); + const { workspace, filesystem } = makeDaytonaWorkspace(); + + await fn(workspace, null); + + expect(filesystem.writeFile).not.toHaveBeenCalled(); + expect(fs.runInSandbox).not.toHaveBeenCalled(); + }); + + it('skips the local provider even with a non-empty bundle', async () => { + const { fn, fs } = loadWriteCuratedExamples(); + const writeFile = jest.fn, [string, string | Buffer, { recursive?: boolean }?]>( + async () => {}, + ); + const workspace = { + filesystem: { + provider: 'local', + basePath: '/sandbox', + writeFile, + mkdir: jest.fn, [string, { recursive?: boolean }?]>(async () => {}), + }, + } as unknown as SandboxWorkspace; + + await fn(workspace, { archive: ARCHIVE, version: '"v1"' }); + + expect(writeFile).not.toHaveBeenCalled(); + expect(fs.runInSandbox).not.toHaveBeenCalled(); + }); +}); + describe('formatNodeCatalogLine', () => { it('should format a basic node with a string version', () => { const node: SearchableNodeDescription = { diff --git a/packages/@n8n/instance-ai/src/workspace/builder-sandbox-factory.ts b/packages/@n8n/instance-ai/src/workspace/builder-sandbox-factory.ts index d7d427eb54d..aee1462e4e3 100644 --- a/packages/@n8n/instance-ai/src/workspace/builder-sandbox-factory.ts +++ b/packages/@n8n/instance-ai/src/workspace/builder-sandbox-factory.ts @@ -343,7 +343,8 @@ export class BuilderSandboxFactory { // Curated examples — also too large to bake into the image, written // post-creation. Without this the builder sees an empty examples/ dir. - await writeCuratedExamples(workspace, this.logger); + const templatesBundle = (await context.templatesService?.getBundle()) ?? null; + await writeCuratedExamples(workspace, templatesBundle, this.logger); await this.linkWorkspaceSdkIfEnabled(workspace, root); @@ -399,7 +400,8 @@ export class BuilderSandboxFactory { await writeFileViaSandbox(workspace, `${root}/node-types/index.txt`, catalog); } - await writeCuratedExamples(workspace, this.logger); + const templatesBundle = (await context.templatesService?.getBundle()) ?? null; + await writeCuratedExamples(workspace, templatesBundle, this.logger); await this.linkWorkspaceSdkIfEnabled(workspace, root); diff --git a/packages/@n8n/instance-ai/src/workspace/builder-templates-service.ts b/packages/@n8n/instance-ai/src/workspace/builder-templates-service.ts new file mode 100644 index 00000000000..39cb56e7c54 --- /dev/null +++ b/packages/@n8n/instance-ai/src/workspace/builder-templates-service.ts @@ -0,0 +1,616 @@ +/** + * Builder templates service: fetches the curated workflow-template bundle from + * the n8n-sdk-templates CDN as a single `templates.tar.gz`, caches it on disk, + * and hands the raw bytes to the sandbox where `tar -xzf` expands them into + * `examples/`. No host-side extraction. + * + * The archive is produced by `n8n-io/n8n-sdk-templates` and is flat: + * - `index.txt` — pipe-delimited catalog used for grep-style lookup + * - `.ts` — one pre-rendered SDK file per publishable template + * + * Versioning: + * - The companion repo emits one archive per supported SDK minor and + * uploads it to `/v./templates.tar.gz`. The newest minor + * is mirrored to `/latest/templates.tar.gz`. + * - The instance derives its CDN path from the bundled `@n8n/workflow-sdk` + * version and prefers that exact path. On 404 (no archive published for + * this minor yet) the service falls back to `/latest/`. Other transport + * failures keep the existing cached bundle and do not trigger fallback. + * - The current channel (`exact` or `latest`) is persisted on disk so warm + * restarts pick the same URL on refresh and the cached ETag is only + * echoed back to its originating path. + * + * Behaviour: + * - First call: read disk cache if present; otherwise do a blocking fetch + * with a hard timeout. On any fetch error, return an empty bundle. + * - Subsequent calls: return memoised bundle synchronously. If the disk + * cache is older than the TTL, fire a background refresh. + * - Refresh: GET `templates.tar.gz` with `If-None-Match`. On 304 just bump + * the timestamp; on 200 atomically swap the cache. On any failure keep + * the existing bundle. + * - Cold-start retry: the initial (blocking) refresh retries transient + * errors (network/5xx/408/429) with exponential backoff. Background + * refreshes stay single-attempt to avoid log spam on persistent outages. + * - Integrity: alongside `templates.tar.gz` the CDN serves + * `templates.tar.gz.sha256` (hex digest). When present, every fresh + * bundle and every disk-loaded cache is verified against it. On mismatch + * the bundle is rejected; on 404 we proceed and warn. This guards + * against transport corruption and accidental CDN inconsistency — not + * against tampering, since the sidecar shares the archive's trust root. + * + * The HTTP ETag is exposed via `getVersion()` prefixed with the channel + * (`v0.15:` or `latest:`) so telemetry can track template-set + * revisions and fallback rate. + * + * Never throws. + */ +import workflowSdkPackage from '@n8n/workflow-sdk/package.json'; +import { createHash } from 'node:crypto'; +import * as fsp from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import type { Logger } from '../logger'; + +const DEFAULT_CDN_BASE_URL = 'https://sdk-templates.n8n.io'; +const WORKFLOW_SDK_VERSION = (workflowSdkPackage as { version: string }).version; +const DEFAULT_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000; +const DEFAULT_FETCH_TIMEOUT_MS = 30_000; +const DEFAULT_MAX_ATTEMPTS = 3; +const DEFAULT_RETRY_BACKOFF_BASE_MS = 1_000; +const DEFAULT_FAILURE_RETRY_INTERVAL_MS = 5 * 60 * 1000; +const RETRY_BACKOFF_CAP_MS = 5_000; +const DEFAULT_CACHE_SUBDIR = 'n8n-sdk-templates'; +const ARCHIVE_FILENAME = 'templates.tar.gz'; +const ETAG_FILENAME = 'etag.txt'; +const SHA256_FILENAME = 'templates.tar.gz.sha256'; +const CHANNEL_FILENAME = 'channel.txt'; + +export interface BuilderTemplatesServiceOptions { + /** + * CDN root. The service appends a channel prefix: + * - `/v./templates.tar.gz` (matched to `sdkVersion`) + * - `/latest/templates.tar.gz` (404-fallback) + */ + cdnBaseUrl?: string; + /** + * SDK version the instance is running, used to build `/v./`. + * Defaults to the version of `@n8n/workflow-sdk` resolved at module load. + */ + sdkVersion?: string; + /** Directory where the service persists the archive + ETag + sha sidecar between runs. */ + cacheDir?: string; + /** Time-to-live before a refresh fires in the background. Default 24h. */ + refreshIntervalMs?: number; + /** Per-request timeout for HTTP fetches. Default 30s. */ + fetchTimeoutMs?: number; + /** Max attempts for the cold-start refresh on transient failures. Default 3. */ + maxAttempts?: number; + /** Base for the exponential retry backoff (capped at 5s). Default 1s. */ + retryBackoffBaseMs?: number; + /** Minimum delay after a failed cold-start hydrate before trying again. Default 5m. */ + failureRetryIntervalMs?: number; + /** When true, the service short-circuits to an empty bundle and never fetches. */ + disabled?: boolean; + /** Optional structured logger. */ + logger?: Logger; +} + +export interface BuilderTemplatesBundle { + /** Raw .tar.gz bytes for the sandbox to extract. Null when no bundle is loaded. */ + archive: Buffer | null; + /** ETag of the archive (content-hashed by R2), or null when no bundle has been loaded. */ + version: string | null; +} + +const EMPTY_BUNDLE: BuilderTemplatesBundle = { archive: null, version: null }; + +type Channel = 'exact' | 'latest'; + +interface CacheState { + bundle: BuilderTemplatesBundle; + lastFetched: number; + /** sha256 hex of the archive currently in `bundle`, when known. */ + sha256: string | null; + /** Which CDN folder the cached bundle came from. */ + channel: Channel; +} + +interface FetchedBundle { + bundle: BuilderTemplatesBundle; + sha256: string | null; +} + +export class BuilderTemplatesService { + private readonly cdnBase: string; + private readonly versionPrefix: string; + private readonly sdkVersion: string; + private readonly cacheDir: string; + private readonly refreshIntervalMs: number; + private readonly fetchTimeoutMs: number; + private readonly maxAttempts: number; + private readonly retryBackoffBaseMs: number; + private readonly failureRetryIntervalMs: number; + private readonly disabled: boolean; + private readonly logger?: Logger; + + private state: CacheState | null = null; + private hydratePromise: Promise | null = null; + private backgroundRefresh: Promise | null = null; + private lastHydrateFailureAt: number | null = null; + + constructor(opts: BuilderTemplatesServiceOptions = {}) { + this.cdnBase = (opts.cdnBaseUrl ?? DEFAULT_CDN_BASE_URL).replace(/\/+$/, ''); + this.sdkVersion = opts.sdkVersion ?? WORKFLOW_SDK_VERSION; + this.versionPrefix = sdkVersionToPrefix(this.sdkVersion); + this.cacheDir = opts.cacheDir ?? path.join(os.homedir(), '.n8n', DEFAULT_CACHE_SUBDIR); + this.refreshIntervalMs = opts.refreshIntervalMs ?? DEFAULT_REFRESH_INTERVAL_MS; + this.fetchTimeoutMs = opts.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS; + this.maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS; + this.retryBackoffBaseMs = opts.retryBackoffBaseMs ?? DEFAULT_RETRY_BACKOFF_BASE_MS; + this.failureRetryIntervalMs = opts.failureRetryIntervalMs ?? DEFAULT_FAILURE_RETRY_INTERVAL_MS; + this.disabled = opts.disabled ?? false; + this.logger = opts.logger; + } + + private channelPrefix(channel: Channel): string { + return channel === 'exact' ? this.versionPrefix : 'latest'; + } + + private archiveUrlFor(channel: Channel): string { + return `${this.cdnBase}/${this.channelPrefix(channel)}/${ARCHIVE_FILENAME}`; + } + + private sha256UrlFor(channel: Channel): string { + return `${this.cdnBase}/${this.channelPrefix(channel)}/${SHA256_FILENAME}`; + } + + /** Return the memoised bundle, hydrating from disk or network on first call. */ + async getBundle(): Promise { + if (this.disabled) return EMPTY_BUNDLE; + + if (!this.state) { + if (!this.hydratePromise) { + if (this.isWithinHydrateFailureCooldown()) return EMPTY_BUNDLE; + this.hydratePromise = this.hydrate(); + } + await this.hydratePromise; + + if (this.state) { + this.lastHydrateFailureAt = null; + } else { + this.lastHydrateFailureAt = Date.now(); + this.hydratePromise = null; + return EMPTY_BUNDLE; + } + } + + const state = this.state; + if (!state) return EMPTY_BUNDLE; + + if (Date.now() - state.lastFetched > this.refreshIntervalMs && !this.backgroundRefresh) { + this.backgroundRefresh = this.refresh({ isInitial: false }).finally(() => { + this.backgroundRefresh = null; + }); + } + + return state.bundle; + } + + private isWithinHydrateFailureCooldown(): boolean { + if (this.lastHydrateFailureAt === null) return false; + return Date.now() - this.lastHydrateFailureAt < this.failureRetryIntervalMs; + } + + /** + * Return the bundle version for telemetry: the underlying ETag stripped of + * its `W/` weak prefix and surrounding double quotes. The raw ETag is kept + * in `state.bundle.version` so `If-None-Match` echoes back the server's + * exact token. + */ + getVersion(): string | null { + const state = this.state; + const raw = state?.bundle.version ?? null; + if (!raw || !state) return null; + const normalised = raw.replace(/^W\//, '').replace(/^"|"$/g, ''); + return `${this.channelPrefix(state.channel)}:${normalised}`; + } + + private async hydrate(): Promise { + const fromDisk = await this.loadFromDisk(); + if (fromDisk) { + this.state = fromDisk; + if (Date.now() - fromDisk.lastFetched > this.refreshIntervalMs) { + this.backgroundRefresh = this.refresh({ isInitial: false }).finally(() => { + this.backgroundRefresh = null; + }); + } + return; + } + await this.refresh({ isInitial: true }); + } + + private async loadFromDisk(): Promise { + const archivePath = path.join(this.cacheDir, ARCHIVE_FILENAME); + try { + const stat = await fsp.stat(archivePath); + const buffer = await fsp.readFile(archivePath); + const actualSha = sha256Hex(buffer); + const expectedSha = await this.readSha256FromDisk(); + + if (expectedSha && expectedSha !== actualSha) { + this.logger?.warn('[builder-templates] disk cache sha256 mismatch, dropping cache', { + expected: expectedSha, + actual: actualSha, + }); + return null; + } + + const channel = await this.readChannelFromDisk(); + if (!channel) { + // Pre-versioned cache layout (no channel.txt). We can't tell which + // CDN folder this archive came from, so its etag is unsafe to echo + // back in If-None-Match. Drop the cache and let the next refresh + // repopulate from scratch. + this.logger?.debug( + '[builder-templates] disk cache missing channel.txt, treating as legacy and refetching', + ); + return null; + } + + const etag = await this.readEtagFromDisk(); + return { + bundle: { archive: buffer, version: etag }, + lastFetched: stat.mtimeMs, + sha256: actualSha, + channel, + }; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + this.logger?.warn('[builder-templates] failed to load disk cache', { + error: error instanceof Error ? error.message : String(error), + }); + } + return null; + } + } + + private async readEtagFromDisk(): Promise { + try { + const raw = await fsp.readFile(path.join(this.cacheDir, ETAG_FILENAME), 'utf-8'); + return raw.trim() || null; + } catch { + return null; + } + } + + private async readSha256FromDisk(): Promise { + try { + const raw = await fsp.readFile(path.join(this.cacheDir, SHA256_FILENAME), 'utf-8'); + return parseSha256(raw); + } catch { + return null; + } + } + + private async readChannelFromDisk(): Promise { + try { + const raw = (await fsp.readFile(path.join(this.cacheDir, CHANNEL_FILENAME), 'utf-8')).trim(); + if (raw === 'exact' || raw === 'latest') return raw; + return null; + } catch { + return null; + } + } + + private async refresh({ isInitial }: { isInitial: boolean }): Promise { + try { + const maxAttempts = isInitial ? this.maxAttempts : 1; + + let outcome = await this.fetchBundleWithRetries('exact', maxAttempts); + let channel: Channel = 'exact'; + + if (outcome.kind === 'not-found') { + this.logger?.warn( + '[builder-templates] no archive at /v/, falling back to /latest/', + { sdkVersion: this.sdkVersion }, + ); + outcome = await this.fetchBundleWithRetries('latest', maxAttempts); + channel = 'latest'; + } + + if (outcome.kind !== 'fetched') return; + + await this.persist( + outcome.bundle.bundle.archive, + outcome.bundle.bundle.version, + outcome.bundle.sha256, + channel, + ); + this.state = { + bundle: outcome.bundle.bundle, + lastFetched: Date.now(), + sha256: outcome.bundle.sha256, + channel, + }; + } catch (error) { + this.logger?.warn('[builder-templates] refresh failed', { + error: error instanceof Error ? error.message : String(error), + }); + } + } + + private async fetchBundleWithRetries( + channel: Channel, + maxAttempts: number, + ): Promise< + | { kind: 'fetched'; bundle: FetchedBundle } + | { kind: 'not-modified' } + | { kind: 'not-found' } + | { kind: 'failed' } + > { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const outcome = await this.tryFetchBundleOnce(channel); + if (outcome.kind === 'fetched') return outcome; + if (outcome.kind === 'not-modified') return outcome; + if (outcome.kind === 'not-found') return outcome; + if (!outcome.retryable || attempt === maxAttempts) return { kind: 'failed' }; + + const delay = Math.min(this.retryBackoffBaseMs * 2 ** (attempt - 1), RETRY_BACKOFF_CAP_MS); + await sleep(delay); + } + return { kind: 'failed' }; + } + + private async tryFetchBundleOnce( + channel: Channel, + ): Promise< + | { kind: 'fetched'; bundle: FetchedBundle } + | { kind: 'not-modified' } + | { kind: 'not-found' } + | { kind: 'failed'; retryable: boolean } + > { + const archiveUrl = this.archiveUrlFor(channel); + + // Only send a conditional request when we already have a bundle in + // memory from the SAME channel — etags from /v/ don't match + // /latest/ even when the file is byte-identical, since R2 hashes per + // path. Sending If-None-Match with an orphan etag would also risk a + // stale 304 that leaves the service empty for the process. + const headers: Record = {}; + const cachedEtag = this.state?.channel === channel ? (this.state.bundle.version ?? null) : null; + if (cachedEtag) headers['If-None-Match'] = cachedEtag; + + let response: Response; + try { + response = await fetch(archiveUrl, { + headers, + signal: AbortSignal.timeout(this.fetchTimeoutMs), + }); + } catch (error) { + // Network / abort errors — assume transient. + this.logger?.warn('[builder-templates] archive fetch threw', { + error: error instanceof Error ? error.message : String(error), + url: archiveUrl, + }); + return { kind: 'failed', retryable: true }; + } + + if (response.status === 304 && this.state?.channel === channel) { + await touchArchiveFile(path.join(this.cacheDir, ARCHIVE_FILENAME)); + this.state = { ...this.state, lastFetched: Date.now() }; + return { kind: 'not-modified' }; + } + + if (response.status === 404) { + // 404 is the unique trigger for fallback — the folder simply isn't + // published. Other non-OK statuses are transport-level failures. + return { kind: 'not-found' }; + } + + if (!response.ok) { + this.logger?.warn('[builder-templates] archive fetch returned non-OK', { + status: response.status, + url: archiveUrl, + }); + return { kind: 'failed', retryable: isRetryableStatus(response.status) }; + } + + const buffer = Buffer.from(await response.arrayBuffer()); + const actualSha = sha256Hex(buffer); + const expectedSha = await this.fetchSha256Sidecar(channel); + + if (expectedSha && expectedSha !== actualSha) { + this.logger?.warn('[builder-templates] sha256 mismatch on downloaded archive, rejecting', { + expected: expectedSha, + actual: actualSha, + url: archiveUrl, + }); + // Treat as a hard failure that isn't worth retrying — the sidecar + // and archive come from the same origin, so a retry will almost + // certainly return the same mismatched pair. + return { kind: 'failed', retryable: false }; + } + + const etag = normaliseEtag(response.headers.get('etag')); + return { + kind: 'fetched', + bundle: { + bundle: { archive: buffer, version: etag }, + sha256: actualSha, + }, + }; + } + + private async fetchSha256Sidecar(channel: Channel): Promise { + const sha256Url = this.sha256UrlFor(channel); + try { + const response = await fetch(sha256Url, { + signal: AbortSignal.timeout(this.fetchTimeoutMs), + }); + if (response.status === 404) { + this.logger?.warn( + '[builder-templates] sha256 sidecar missing — proceeding without integrity check', + { url: sha256Url }, + ); + return null; + } + if (!response.ok) { + this.logger?.warn( + '[builder-templates] sha256 sidecar fetch returned non-OK — proceeding without integrity check', + { status: response.status, url: sha256Url }, + ); + return null; + } + return parseSha256(await response.text()); + } catch (error) { + this.logger?.warn( + '[builder-templates] sha256 sidecar fetch threw — proceeding without integrity check', + { + error: error instanceof Error ? error.message : String(error), + url: sha256Url, + }, + ); + return null; + } + } + + private async persist( + buffer: Buffer | null, + etag: string | null, + sha256: string | null, + channel: Channel, + ): Promise { + if (!buffer) return; + await fsp.mkdir(this.cacheDir, { recursive: true }); + // Write metadata first, payload last. If we crash between the metadata + // write and the archive write, the disk is left in an "orphan metadata" + // state — `loadFromDisk` will see no archive → return null → next + // refresh goes out unconditionally (no stale If-None-Match echoed back). + if (etag) { + await atomicWriteFile(path.join(this.cacheDir, ETAG_FILENAME), etag); + } else { + await unlinkIfExists(path.join(this.cacheDir, ETAG_FILENAME)); + } + if (sha256) { + await atomicWriteFile(path.join(this.cacheDir, SHA256_FILENAME), sha256); + } else { + await unlinkIfExists(path.join(this.cacheDir, SHA256_FILENAME)); + } + await atomicWriteFile(path.join(this.cacheDir, CHANNEL_FILENAME), channel); + await atomicWriteFile(path.join(this.cacheDir, ARCHIVE_FILENAME), buffer); + } +} + +/** + * Turn an SDK version like `0.15.0` (or `0.15.0-beta.3`) into the `v0.15` + * channel prefix used in the CDN URL. Falls back to `latest` if the version + * can't be parsed — defensive against unexpected pkg.json shapes at boot. + */ +function sdkVersionToPrefix(sdkVersion: string): string { + const match = sdkVersion.match(/^(\d+)\.(\d+)/); + if (!match) return 'latest'; + return `v${match[1]}.${match[2]}`; +} + +function normaliseEtag(raw: string | null): string | null { + if (!raw) return null; + const trimmed = raw.trim(); + if (!trimmed) return null; + // R2 emits weak ETags as `W/"hex"` — keep the full token so `If-None-Match` + // echoes it verbatim and the server can match. + return trimmed; +} + +function sha256Hex(buffer: Buffer): string { + return createHash('sha256').update(buffer).digest('hex'); +} + +/** + * Parse a sha256 sidecar body. Accepts either a bare hex digest or the + * ` ` format `sha256sum` emits. Returns the lowercased hex + * digest, or `null` if the body is empty/malformed. + */ +function parseSha256(raw: string): string | null { + const first = raw.trim().split(/\s+/, 1)[0]; + if (!first || !/^[0-9a-fA-F]{64}$/.test(first)) return null; + return first.toLowerCase(); +} + +function isRetryableStatus(status: number): boolean { + if (status >= 500) return true; + return status === 408 || status === 429; +} + +async function sleep(ms: number): Promise { + await new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function atomicWriteFile(target: string, contents: Buffer | string): Promise { + const tmp = `${target}.tmp-${process.pid}-${Date.now()}`; + try { + await fsp.writeFile(tmp, contents); + await fsp.rename(tmp, target); + } catch (error) { + try { + await fsp.unlink(tmp); + } catch { + // best-effort cleanup + } + throw error; + } +} + +async function unlinkIfExists(target: string): Promise { + try { + await fsp.unlink(target); + } catch { + // best-effort cleanup + } +} + +async function touchArchiveFile(target: string): Promise { + const now = new Date(); + try { + await fsp.utimes(target, now, now); + } catch { + // non-fatal — next refresh will reset state.lastFetched anyway + } +} + +/** + * Read the env-driven configuration into a `BuilderTemplatesServiceOptions`. + * Returned options can be overridden at the call site. + * + * Invalid `N8N_INSTANCE_AI_TEMPLATES_REFRESH_HOURS` values are warned about and + * dropped so the constructor's default kicks in — otherwise `Number("abc")` + * would yield `NaN` and silently disable refreshes. + */ +export function builderTemplatesOptionsFromEnv({ + logger, +}: { logger?: Logger } = {}): BuilderTemplatesServiceOptions { + const url = process.env.N8N_INSTANCE_AI_TEMPLATES_URL; + const hoursRaw = process.env.N8N_INSTANCE_AI_TEMPLATES_REFRESH_HOURS; + const disabled = process.env.N8N_INSTANCE_AI_TEMPLATES_DISABLED; + + const refreshIntervalMs = parseRefreshHoursMs(hoursRaw, logger); + + return { + ...(url ? { cdnBaseUrl: url } : {}), + ...(refreshIntervalMs !== null ? { refreshIntervalMs } : {}), + disabled: disabled === '1' || disabled?.toLowerCase() === 'true', + }; +} + +function parseRefreshHoursMs(raw: string | undefined, logger?: Logger): number | null { + if (raw === undefined || raw === '') return null; + const hours = Number(raw); + if (!Number.isFinite(hours) || hours <= 0) { + logger?.warn( + '[builder-templates] ignoring invalid N8N_INSTANCE_AI_TEMPLATES_REFRESH_HOURS, using default', + { value: raw }, + ); + return null; + } + return hours * 60 * 60 * 1000; +} diff --git a/packages/@n8n/instance-ai/src/workspace/sandbox-fs.ts b/packages/@n8n/instance-ai/src/workspace/sandbox-fs.ts index 472b095be7d..94bf34aebd0 100644 --- a/packages/@n8n/instance-ai/src/workspace/sandbox-fs.ts +++ b/packages/@n8n/instance-ai/src/workspace/sandbox-fs.ts @@ -127,8 +127,11 @@ export async function writeFileViaSandbox( await runWriteCommand(`printf '%s' '${chunk}' >> '${escapedTempPath}'`); } + // Decode + cleanup in one shell expression; the exit reflects base64's + // status. Avoid the variable name `status` — it's a read-only builtin in + // zsh, which silently breaks the assignment and loses base64's exit code. await runWriteCommand( - `base64 -d '${escapedTempPath}' > '${escapeSingleQuotes(filePath)}'; status=$?; rm -f '${escapedTempPath}'; exit $status`, + `base64 -d '${escapedTempPath}' > '${escapeSingleQuotes(filePath)}'; rc=$?; rm -f '${escapedTempPath}'; exit $rc`, ); } diff --git a/packages/@n8n/instance-ai/src/workspace/sandbox-setup.ts b/packages/@n8n/instance-ai/src/workspace/sandbox-setup.ts index 9fd74d3f56a..247a51f05c4 100644 --- a/packages/@n8n/instance-ai/src/workspace/sandbox-setup.ts +++ b/packages/@n8n/instance-ai/src/workspace/sandbox-setup.ts @@ -21,11 +21,12 @@ * *.ts # reusable node/workflow modules */ -import { getExampleFiles, type ExampleFile } from '@n8n/workflow-sdk/examples-loader'; import { createRequire } from 'node:module'; +import { gunzipSync } from 'node:zlib'; import type { Logger } from '../logger'; import type { InstanceAiContext, SearchableNodeDescription } from '../types'; +import type { BuilderTemplatesBundle } from './builder-templates-service'; import { isLinkWorkspaceSdkEnabled, packWorkspaceSdk, @@ -46,6 +47,9 @@ const NOOP_LOGGER: Logger = { error: () => {}, debug: () => {}, }; +const TAR_BLOCK_SIZE = 512; +const TAR_TYPE_REGULAR = '0'; +const TEMPLATE_ENTRY_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\.ts$/; type SandboxWorkspaceSetupStep = | 'resolve-workspace-root' @@ -484,43 +488,166 @@ export async function getWorkspaceRoot(workspace: SandboxWorkspace): Promise.ts`). Rejecting + * everything else prevents path traversal, symlink/hardlink writes, and nested + * output when the sandbox later runs `tar -xzf`. + */ +function validateBuilderTemplatesArchive(archive: Buffer): string | null { + let tar: Buffer; + try { + tar = gunzipSync(archive); + } catch (error) { + return `failed to gunzip archive: ${getErrorMessage(error)}`; + } + + let offset = 0; + while (offset + TAR_BLOCK_SIZE <= tar.length) { + const header = tar.subarray(offset, offset + TAR_BLOCK_SIZE); + // A zero header marks the end of a tar archive. We do not require the + // optional second zero block because `tar` itself accepts archives with + // one terminator, and this is only a preflight guard before extraction. + if (isZeroBlock(header)) return null; + + // USTAR stores long path components as `prefix` + `name`. Combining them + // before validation ensures nested or absolute paths cannot hide in either + // field independently. + const name = readTarString(header, 0, 100); + const prefix = readTarString(header, 345, 155); + const entryName = prefix ? `${prefix}/${name}` : name; + const typeFlag = readTarString(header, 156, 1); + const size = parseTarOctal(header, 124, 12); + + if (size === null) return `invalid size for archive entry "${entryName}"`; + // Empty type is the old tar spelling for a regular file; `0` is the USTAR + // spelling. All other types include directories, symlinks, hardlinks, and + // metadata extensions, none of which belong in the curated bundle. + if (typeFlag !== '' && typeFlag !== TAR_TYPE_REGULAR) { + return `unsupported archive entry type "${typeFlag}" for "${entryName}"`; + } + if (!isAllowedTemplateEntryName(entryName)) { + return `unsupported archive entry path "${entryName}"`; + } + + // Tar payloads are padded to 512-byte blocks, so jump over the file content + // plus padding to land exactly on the next header. + const dataBlocks = Math.ceil(size / TAR_BLOCK_SIZE); + offset += TAR_BLOCK_SIZE + dataBlocks * TAR_BLOCK_SIZE; + } + + return offset === tar.length ? null : 'trailing partial tar header'; +} + +function isAllowedTemplateEntryName(name: string): boolean { + if (name === 'index.txt') return true; + return TEMPLATE_ENTRY_PATTERN.test(name); +} + +function isZeroBlock(block: Buffer): boolean { + return block.every((byte) => byte === 0); +} + +function readTarString(block: Buffer, start: number, length: number): string { + const field = block.subarray(start, start + length); + const nullIndex = field.indexOf(0); + return field.subarray(0, nullIndex === -1 ? field.length : nullIndex).toString('utf-8'); +} + +function parseTarOctal(block: Buffer, start: number, length: number): number | null { + const raw = readTarString(block, start, length).trim(); + if (!/^[0-7]+$/.test(raw)) return null; + const parsed = Number.parseInt(raw, 8); + return Number.isSafeInteger(parsed) ? parsed : null; +} + +/** + * Write the curated workflow examples archive into `${root}/examples/`. * - * Used by `setupSandboxWorkspace` (local provider) and by the Daytona / - * n8n-sandbox factory paths, which skip the full setup but still need the - * curated reference material the builder agent greps against. + * Used by the Daytona / n8n-sandbox factory paths. The local provider + * deliberately skips this — dev iteration on the SDK doesn't need the + * curated reference set, and the agent there operates fine without it + * (same fallback as a cold start with the CDN unreachable). * - * No-op when the loader returns an empty bundle (e.g. running against a - * workspace where the manifest hasn't been fetched). + * The CDN payload is a flat `.tar.gz` of `.ts` + `index.txt`. We + * write the bytes into the sandbox and run `tar -xzf` in-sandbox to + * expand them into `examples/` — far cheaper than 100+ individual + * `writeFile` round-trips for remote providers. The archive file is + * removed after extraction so it doesn't leak into the agent's view. + * + * No-op when the bundle is empty (e.g. `templatesService` was not + * configured, or the CDN fetch failed and there was no disk cache). */ export async function writeCuratedExamples( workspace: SandboxWorkspace, + bundle: BuilderTemplatesBundle | null, logger?: Logger, ): Promise { - const start = Date.now(); - // Examples are nice-to-have — never block the build when loading them fails. - let exampleFiles: ExampleFile[]; - let indexTxt: string; - try { - ({ files: exampleFiles, indexTxt } = getExampleFiles()); - } catch (error) { - logger?.warn('[sandbox-setup] curated examples unavailable, continuing without', { - error: error instanceof Error ? error.message : String(error), + if (!bundle?.archive) return; + + if (workspace.filesystem?.provider === 'local') { + logger?.debug('[sandbox-setup] skipping curated examples for local provider'); + return; + } + + // Defense-in-depth for the curated CDN bundle. This validates the narrow + // archive shape we publish, not arbitrary user-supplied tar files. + const validationError = validateBuilderTemplatesArchive(bundle.archive); + if (validationError) { + logger?.warn('[sandbox-setup] rejected curated examples archive', { + error: validationError, + archiveBytes: bundle.archive.byteLength, + archiveVersion: bundle.version, }); return; } - if (exampleFiles.length === 0) return; + const start = Date.now(); const root = await getWorkspaceRoot(workspace); - const fileMap = new Map(); - fileMap.set('examples/index.txt', indexTxt); - for (const example of exampleFiles) { - fileMap.set(`examples/${example.filename}`, example.content); + const archivePath = `${root}/.templates.tar.gz`; + const examplesDir = `${root}/examples`; + + if (workspace.filesystem) { + await workspace.filesystem.mkdir(examplesDir, { recursive: true }); + await workspace.filesystem.writeFile(archivePath, bundle.archive, { recursive: true }); + } else { + const mkdirResult = await runInSandbox( + workspace, + `mkdir -p '${escapeSingleQuotes(examplesDir)}'`, + ); + if (mkdirResult.exitCode !== 0) { + logger?.warn('[sandbox-setup] failed to create examples/ dir', { + stderr: mkdirResult.stderr, + }); + return; + } + await writeFileViaSandbox(workspace, archivePath, bundle.archive); + } + + // Extract and clean up in one command so a partial state isn't left + // behind if `tar` exits non-zero. `rm -f` is always run; the exec's + // status is `tar`'s exit code. `2>&1` folds tar's stderr into stdout so + // the failure cause is still visible if the sandbox runtime drops stderr. + // Avoid the variable name `status` — it's a read-only builtin in zsh. + const extract = await runInSandbox( + workspace, + `tar -xzf '${escapeSingleQuotes(archivePath)}' -C '${escapeSingleQuotes(examplesDir)}' 2>&1; rc=$?; rm -f '${escapeSingleQuotes(archivePath)}'; exit $rc`, + ); + if (extract.exitCode !== 0) { + logger?.warn('[sandbox-setup] failed to extract curated examples', { + exitCode: extract.exitCode, + stderr: extract.stderr, + stdout: extract.stdout, + archivePath, + archiveBytes: bundle.archive.byteLength, + archiveVersion: bundle.version, + }); + return; } - await writeWorkspaceFiles(workspace, root, fileMap); logger?.debug('[sandbox-setup] prepared curated examples', { - count: exampleFiles.length, + bytes: bundle.archive.byteLength, + version: bundle.version, durationMs: Date.now() - start, }); } @@ -596,7 +723,12 @@ export async function setupSandboxWorkspace( ); await setupStep( 'write-curated-examples', - async () => await writeCuratedExamples(workspace, context.logger), + async () => + await writeCuratedExamples( + workspace, + (await context.templatesService?.getBundle()) ?? null, + context.logger, + ), ); // npm install (must run after package.json is in place) diff --git a/packages/@n8n/instance-ai/src/workspace/template-telemetry.test.ts b/packages/@n8n/instance-ai/src/workspace/template-telemetry.test.ts index 4e214369c9e..8eb65cc6f24 100644 --- a/packages/@n8n/instance-ai/src/workspace/template-telemetry.test.ts +++ b/packages/@n8n/instance-ai/src/workspace/template-telemetry.test.ts @@ -317,10 +317,9 @@ describe('createTypedToolObserver', () => { }), ); observe( - toolResult( - 'tc-1', - '/workspace/examples/slack-daily-summary.ts (200 bytes)\nfile content here\n', - ), + toolResult('tc-1', { + content: '/workspace/examples/slack-daily-summary.ts (200 bytes)\nfile content here\n', + }), ); const read = calls.find((c) => c.name === 'Builder template read'); @@ -329,13 +328,31 @@ describe('createTypedToolObserver', () => { expect(read!.props.bytes_read).toBeGreaterThan(0); }); + it('emits typed read for legacy bare string results', () => { + const { opts, calls } = makeOpts(); + const session = createTemplateTelemetrySession(opts); + const observe = createTypedToolObserver(session); + + observe(toolCall('tc-legacy-read', 'workspace_read_file', { path: 'examples/foo.ts' })); + observe(toolResult('tc-legacy-read', 'file content here')); + + const read = calls.find((c) => c.name === 'Builder template read'); + expect(read).toBeDefined(); + expect(read!.props.template_filename).toBe('foo.ts'); + expect(read!.props.bytes_read).toBe('file content here'.length); + }); + it('emits typed search for workspace_grep targeting examples/', () => { const { opts, calls } = makeOpts(); const session = createTemplateTelemetrySession(opts); const observe = createTypedToolObserver(session); observe(toolCall('tc-2', 'workspace_grep', { pattern: 'slack', path: 'examples/' })); - observe(toolResult('tc-2', 'examples/a.ts:1:1: slack\nexamples/b.ts:5:1: slack\n')); + observe( + toolResult('tc-2', { + content: 'examples/a.ts:1:1: slack\nexamples/b.ts:5:1: slack\n', + }), + ); const search = calls.find((c) => c.name === 'Builder template search'); expect(search).toBeDefined(); diff --git a/packages/@n8n/instance-ai/src/workspace/template-telemetry.ts b/packages/@n8n/instance-ai/src/workspace/template-telemetry.ts index ffa88ea3b60..f5ab05ef6f7 100644 --- a/packages/@n8n/instance-ai/src/workspace/template-telemetry.ts +++ b/packages/@n8n/instance-ai/src/workspace/template-telemetry.ts @@ -47,6 +47,13 @@ export interface TelemetrySessionOptions { workItemId: string; /** Optional NL request from the user; truncated to 120 chars. */ userRequestExcerpt?: string; + /** + * Version identifier of the curated templates bundle in use this run + * (typically a short git SHA from the n8n-sdk-templates manifest). + * Emitted on every search/read/session event so we can correlate usage + * to specific bundle revisions. + */ + templatesVersion?: string | null; } export function createTemplateTelemetrySession( @@ -56,6 +63,7 @@ export function createTemplateTelemetrySession( thread_id: opts.threadId, run_id: opts.runId, work_item_id: opts.workItemId, + templates_version: opts.templatesVersion ?? null, }; let searchCount = 0; @@ -234,13 +242,13 @@ export function createTypedToolObserver( if (!match) return; pending.delete(event.payload.toolCallId); - const result = event.payload.result; - if (typeof result !== 'string') return; + const resultText = extractTypedToolResultText(event.payload.result); + if (resultText === null) return; if (match.kind === 'read') { - session.observeTypedRead(match.filename, result.length); + session.observeTypedRead(match.filename, resultText.length); } else { - session.observeTypedSearch(match.query, countResultLines(result)); + session.observeTypedSearch(match.query, countResultLines(resultText)); } return; } @@ -269,3 +277,15 @@ function matchTypedTemplateCall( } return undefined; } + +function extractTypedToolResultText(result: unknown): string | null { + if (typeof result === 'string') return result; + if (!isRecord(result)) return null; + + const { content } = result; + return typeof content === 'string' ? content : null; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} diff --git a/packages/@n8n/workflow-sdk/.gitignore b/packages/@n8n/workflow-sdk/.gitignore index 976aee62336..a618f6ee487 100644 --- a/packages/@n8n/workflow-sdk/.gitignore +++ b/packages/@n8n/workflow-sdk/.gitignore @@ -12,11 +12,3 @@ test-fixtures/real-workflows/*.generated.ts scripts/*.js scripts/*.js.map scripts/*.d.ts - -# Curation pipeline intermediates (committed: manifest.json, templates.zip, _calibration.json, _coverage-report.json) -examples/_raw/ -examples/_failures.log -examples/_catalog-snapshot.json - -# Workflow JSONs are extracted from templates.zip at runtime — only the zip is committed -examples/workflows/ diff --git a/packages/@n8n/workflow-sdk/docs/template-criteria.md b/packages/@n8n/workflow-sdk/docs/template-criteria.md deleted file mode 100644 index 290c0f4f038..00000000000 --- a/packages/@n8n/workflow-sdk/docs/template-criteria.md +++ /dev/null @@ -1,95 +0,0 @@ -# Template helpfulness criteria - -This rubric scores public n8n.io workflows for inclusion in `examples/manifest.json`, -the curated set the instance-ai builder agent grep over while building. The rubric — -not the resulting list — is the durable artifact: re-run `pnpm regenerate-examples` -against a fresh catalog to refresh picks. See `scripts/criteria.ts` for the -implementation; tune weights there. - -## What "helpful" means - -A template is helpful if it (a) reflects a real-world pattern the agent will be -asked to build, (b) is structurally clear enough to reason about, and (c) adds -something the rest of the set doesn't. - -## Mechanical viability gate (drop on fail) - -Hard filters applied before scoring. Anything failing here is dropped, regardless -of other signals. - -| Filter | Rule | Stage | -|---|---|---| -| Published | `status == 'published'` | detail | -| Free | `purchaseUrl == null` AND (`price` is null/undefined OR `price <= 0`) | catalog | -| Size upper bound | `nodes.length <= 40` (list count is sparse) | catalog | -| Size full range | `workflow.nodes.length` in `[3, 40]` | detail | -| Verified author | `user.verified == true` | catalog | -| Has trigger | At least one node whose type identifies as a trigger | detail | - -Verified-author and node-count filters drop ~30% of the catalog combined; the -trade-off is quality density over breadth. - -## Scoring dimensions - -Six dimensions sum to a relative total. Weights are numbers in `criteria.ts`; the -manifest entry records each dimension's score so picks stay reviewable. - -| Dimension | Weight | Signal | -|---|---|---| -| **Real-world traction** | 20 | `log10(totalViews + 1)` + `log10(recentViews + 1)` | -| **Recency** | 20 | linear decay from 1.0 (≤90d since `updatedAt`) to 0.0 (≥2y) | -| **Pattern coverage (marginal)** | 35 | `1.0 / (1 + countInBucket(running_set))` — bucket = `(triggerType, primaryIntegration, hasAI, controlFlowKind)`; recomputed as each pick is accepted | -| **AI-agent relevance** | 0 | Folded into bucket key (`hasAI`); no extra weight. Diversity bucketing alone delivers ~50% AI representation. | -| **Structural clarity** | 15 | `+0.4` if median node has a non-default name (not `Edit Fields1`, `HTTP Request2`); `+0.3` if has ≥1 sticky note; `+0.3` if has ≥3 distinct node types | -| **Pedagogical density** | 5 | `min(1, distinctNodeTypes / nodeCount)` — favours patterns over repetition | - -### Why coverage dominates (35) - -Pattern coverage is the heaviest weight. The builder agent needs to grep -across many distinct shapes (trigger types, integration mixes, control flow -patterns), not 100 copies of "schedule → openAi → slack." Each pick's coverage -score recomputes after the running set updates, so popular-but-redundant -candidates lose ground. - -### Why AI bias is 0 - -The catalog is already ~53% AI-rich. The bucket key includes `hasAI`, so -round-robin selection naturally gives AI workflows ~50% of slots. Adding an -explicit AI bonus on top would over-fit. If telemetry later shows the agent -under-uses AI templates, lift this weight. - -### Why traction is `log10(views) + log10(recentViews)` - -Views distribution is wildly skewed (median 1, max 780k). Log-scale compresses -the long tail into a usable range. Adding recent views (last 30d) catches -"currently popular" alongside "historically popular." - -## Diversity bucket - -`bucketKey(detail) = (triggerType, primaryIntegration, hasAI, controlFlowKind)` - -- `triggerType` ∈ `{webhook, schedule, chatTrigger, formTrigger, manual, telegram, gmail, other}` -- `primaryIntegration` — vendor prefix of the most-frequent non-trigger node (e.g. `googleSheets`, `slack`, `openAi`, `telegram`) -- `hasAI` ∈ `{true, false}` — any `@n8n/n8n-nodes-langchain.*` node OR any `openAi`/`anthropic` chat model -- `controlFlowKind` ∈ `{linear, branching, loop, parallel}` — derived from connections: branching = ifElse/switch present, loop = splitInBatches present, parallel = ≥1 node with multiple downstream connections, else linear - -Selection is a rescoring loop: each round picks the candidate with the highest -total after recomputing scores against the running set. The coverage term -(`1 / (1 + countInBucket)`) biases toward underrepresented buckets without -enforcing strict round-robin. - -## Calibration - -`pnpm criteria:calibrate` runs the rubric against `examples/_calibration.json` -(20–30 hand-tagged workflows) and reports: - -- Spearman correlation between rubric rank and expert verdict -- Top disagreements with explanations - -We tune weights until correlation ≥ 0.7 on the calibration set. - -## Coverage check - -`pnpm criteria:coverage` checks the candidate set against real builder prompts -in `packages/@n8n/instance-ai/evaluations/data/workflows/`. Coverage = % of eval -prompts where at least one template matches ≥2 keywords. Phase 1 ships at ≥70%. diff --git a/packages/@n8n/workflow-sdk/examples/_calibration.json b/packages/@n8n/workflow-sdk/examples/_calibration.json deleted file mode 100644 index a349bf681ed..00000000000 --- a/packages/@n8n/workflow-sdk/examples/_calibration.json +++ /dev/null @@ -1,245 +0,0 @@ -{ - "instructions": "For each entry, set verdict to \"helpful\" | \"borderline\" | \"not-helpful\" and add a one-line rationale. This becomes the calibration set.", - "expert_tagged": [ - { - "id": 6270, - "rank": 1, - "verdict": "TBD", - "rationale": "", - "name": "Build Your First AI Agent", - "triggerType": "chatTrigger", - "hasAI": true, - "nodeCount": 7, - "score": 95, - "source": "https://n8n.io/workflows/6270" - }, - { - "id": 8237, - "rank": 2, - "verdict": "TBD", - "rationale": "", - "name": "Personal Life Manager with Telegram, Google Services & Voice-Enabled AI", - "triggerType": "telegram", - "hasAI": true, - "nodeCount": 12, - "score": 90.95, - "source": "https://n8n.io/workflows/8237" - }, - { - "id": 2753, - "rank": 3, - "verdict": "TBD", - "rationale": "", - "name": "RAG Chatbot for Company Documents using Google Drive and Gemini", - "triggerType": "other", - "hasAI": true, - "nodeCount": 12, - "score": 90.35, - "source": "https://n8n.io/workflows/2753" - }, - { - "id": 4846, - "rank": 4, - "verdict": "TBD", - "rationale": "", - "name": "Generate AI Videos with Google Veo3, Save to Google Drive and Upload to YouTube", - "triggerType": "manual", - "hasAI": true, - "nodeCount": 10, - "score": 90.19, - "source": "https://n8n.io/workflows/4846" - }, - { - "id": 4352, - "rank": 5, - "verdict": "TBD", - "rationale": "", - "name": "AI-Powered Multi-Social Media Post Automation: Google Trends & Perplexity AI ", - "triggerType": "schedule", - "hasAI": true, - "nodeCount": 12, - "score": 89.9, - "source": "https://n8n.io/workflows/4352" - }, - { - "id": 5148, - "rank": 6, - "verdict": "TBD", - "rationale": "", - "name": "Local Chatbot with Retrieval Augmented Generation (RAG)", - "triggerType": "formTrigger", - "hasAI": true, - "nodeCount": 10, - "score": 89.7, - "source": "https://n8n.io/workflows/5148" - }, - { - "id": 4966, - "rank": 7, - "verdict": "TBD", - "rationale": "", - "name": "Customer Support WhatsApp Bot with Google Docs Knowledge Base and Gemini AI", - "triggerType": "other", - "hasAI": true, - "nodeCount": 12, - "score": 89.7, - "source": "https://n8n.io/workflows/4966" - }, - { - "id": 5626, - "rank": 8, - "verdict": "TBD", - "rationale": "", - "name": "Free AI Image Generator - n8n Automation Workflow with Gemini/ChatGPT", - "triggerType": "chatTrigger", - "hasAI": true, - "nodeCount": 10, - "score": 89.61, - "source": "https://n8n.io/workflows/5626" - }, - { - "id": 5338, - "rank": 9, - "verdict": "TBD", - "rationale": "", - "name": "Generate AI Viral Videos with Seedance and Upload to TikTok, YouTube & Instagram", - "triggerType": "schedule", - "hasAI": true, - "nodeCount": 12, - "score": 89.35, - "source": "https://n8n.io/workflows/5338" - }, - { - "id": 4827, - "rank": 10, - "verdict": "TBD", - "rationale": "", - "name": "AI-Powered WhatsApp Chatbot for Text, Voice, Images, and PDF with RAG", - "triggerType": "manual", - "hasAI": true, - "nodeCount": 18, - "score": 89.26, - "source": "https://n8n.io/workflows/4827" - }, - { - "id": 5110, - "rank": 11, - "verdict": "TBD", - "rationale": "", - "name": "Create & Upload AI-Generated ASMR YouTube Shorts with Seedance, Fal AI, and GPT-4", - "triggerType": "schedule", - "hasAI": true, - "nodeCount": 13, - "score": 88.91, - "source": "https://n8n.io/workflows/5110" - }, - { - "id": 5385, - "rank": 12, - "verdict": "TBD", - "rationale": "", - "name": "Lead Generation System: Google Maps to Email Scraper with Google Sheets Export", - "triggerType": "manual", - "hasAI": false, - "nodeCount": 11, - "score": 88.89, - "source": "https://n8n.io/workflows/5385" - }, - { - "id": 5678, - "rank": 13, - "verdict": "TBD", - "rationale": "", - "name": "Automate Email Filtering & AI Summarization. 100% free & effective, works 7/24 ", - "triggerType": "gmail", - "hasAI": true, - "nodeCount": 7, - "score": 88.39, - "source": "https://n8n.io/workflows/5678" - }, - { - "id": 2860, - "rank": 14, - "verdict": "TBD", - "rationale": "", - "name": "AI Automated HR Workflow for CV Analysis and Candidate Evaluation", - "triggerType": "formTrigger", - "hasAI": true, - "nodeCount": 12, - "score": 88.38, - "source": "https://n8n.io/workflows/2860" - }, - { - "id": 5962, - "rank": 15, - "verdict": "TBD", - "rationale": "", - "name": "Track SEO Keyword Rankings with Bright Data MCP and GPT-4o AI Analysis", - "triggerType": "schedule", - "hasAI": true, - "nodeCount": 10, - "score": 88.29, - "source": "https://n8n.io/workflows/5962" - }, - { - "id": 10000, - "rank": 16, - "verdict": "TBD", - "rationale": "", - "name": "Auto-Create TikTok Videos with VEED.io AI Avatars, ElevenLabs & GPT-4", - "triggerType": "telegram", - "hasAI": true, - "nodeCount": 12, - "score": 87.92, - "source": "https://n8n.io/workflows/10000" - }, - { - "id": 3586, - "rank": 17, - "verdict": "TBD", - "rationale": "", - "name": "AI-Powered WhatsApp Chatbot 🤖📲 for Text, Voice, Images & PDFs with memory 🧠", - "triggerType": "other", - "hasAI": true, - "nodeCount": 13, - "score": 87.87, - "source": "https://n8n.io/workflows/3586" - }, - { - "id": 13270, - "rank": 18, - "verdict": "TBD", - "rationale": "", - "name": "Use skills In n8n agent node", - "triggerType": "chatTrigger", - "hasAI": true, - "nodeCount": 12, - "score": 87.62, - "source": "https://n8n.io/workflows/13270" - }, - { - "id": 4484, - "rank": 19, - "verdict": "TBD", - "rationale": "", - "name": "Build a Voice AI Chatbot with ElevenLabs and InfraNodus Knowledge Experts", - "triggerType": "webhook", - "hasAI": true, - "nodeCount": 8, - "score": 87.53, - "source": "https://n8n.io/workflows/4484" - }, - { - "id": 2846, - "rank": 20, - "verdict": "TBD", - "rationale": "", - "name": "AI Voice Chatbot with ElevenLabs & OpenAI for Customer Service and Restaurants", - "triggerType": "manual", - "hasAI": true, - "nodeCount": 14, - "score": 87.44, - "source": "https://n8n.io/workflows/2846" - } - ] -} diff --git a/packages/@n8n/workflow-sdk/examples/_coverage-report.json b/packages/@n8n/workflow-sdk/examples/_coverage-report.json deleted file mode 100644 index 98df51d44b6..00000000000 --- a/packages/@n8n/workflow-sdk/examples/_coverage-report.json +++ /dev/null @@ -1,809 +0,0 @@ -{ - "generatedAt": "2026-05-07T13:26:57.543Z", - "total_prompts": 14, - "covered": 14, - "uncovered": 0, - "coverage": 1, - "target": 0.7, - "passed": true, - "results": [ - { - "prompt_file": "airtable-split-to-slack.json", - "prompt": "Every hour, fetch all records from an Airtable table. Use the HTTP Request node to call GET https://api.airtable.com/v0/app123abc/Tasks with a Bearer token auth header — Airtable responds with a JSON ", - "keywords": [ - "hour", - "records", - "airtable", - "table", - "app123abc", - "tasks", - "bearer", - "token", - "header", - "responds", - "json", - "object", - "shape", - "where", - "record", - "post", - "slack", - "channel", - "daily-tasks", - "containing", - "task", - "status", - "later", - "build", - "schedule", - "split" - ], - "matched": true, - "top_match": { - "slug": "automate-3d-body-model-generation-from-images-using-sam-3d-g-11460", - "matches": 10, - "matched_keywords": [ - "tasks", - "header", - "json", - "shape", - "where", - "post", - "channel", - "task", - "status", - "schedule" - ] - } - }, - { - "prompt_file": "contact-form-automation.json", - "prompt": "Create a workflow that handles contact form submissions via a webhook. It should send an auto-reply email to the person who submitted the form, notify my team on Telegram, and log each submission to G", - "keywords": [ - "handles", - "contact", - "form", - "webhook", - "auto-reply", - "email", - "person", - "who", - "notify", - "team", - "telegram", - "log", - "google", - "sheets", - "documentid", - "1bximvs0xra5nfmdkvbdbzjgmuuqptlbs74ogve2upms", - "sheet", - "later", - "build", - "gmail", - "multi", - "action" - ], - "matched": true, - "top_match": { - "slug": "lead-generation-system-google-maps-to-email-scraper-with-goo-5385", - "matches": 12, - "matched_keywords": [ - "handles", - "contact", - "form", - "email", - "team", - "log", - "google", - "sheets", - "sheet", - "build", - "multi", - "action" - ] - } - }, - { - "prompt_file": "cross-team-linear-report.json", - "prompt": "Get all the Linear issues created in the last 2 weeks. Filter them for issues created for a different team than the one the creator is in. I have this team mapping to use: Alice (alice@company.com) be", - "keywords": [ - "linear", - "issues", - "created", - "last", - "weeks", - "filter", - "different", - "team", - "than", - "creator", - "mapping", - "alice", - "company", - "belongs", - "both", - "frontend", - "bob", - "backend", - "carol", - "store", - "note", - "person", - "belong", - "multiple", - "teams", - "cross-team", - "issue", - "only", - "not", - "list", - "calculate", - "number", - "tickets", - "per", - "post", - "ordered", - "descending", - "slack", - "channel", - "called", - "cross-team-reports", - "later", - "build", - "schedule", - "processing" - ], - "matched": true, - "top_match": { - "slug": "extract-invoice-data-from-email-to-google-sheets-using-gpt-4-4376", - "matches": 19, - "matched_keywords": [ - "linear", - "issues", - "filter", - "different", - "team", - "company", - "note", - "multiple", - "teams", - "issue", - "only", - "not", - "list", - "calculate", - "number", - "per", - "channel", - "schedule", - "processing" - ] - } - }, - { - "prompt_file": "daily-slack-summary.json", - "prompt": "Every day, get the posts made in the past day on 3 different Slack channels: #general (C04GENERAL01), #engineering (C04ENGINEER1), and #product (C04PRODUCT01). Summarize them using AI, and post the su", - "keywords": [ - "day", - "posts", - "made", - "past", - "different", - "slack", - "channels", - "general", - "c04general01", - "engineering", - "c04engineer1", - "product", - "c04product01", - "summarize", - "post", - "summary", - "daily-digest", - "c04dailydg01", - "later", - "build", - "schedule" - ], - "matched": true, - "top_match": { - "slug": "ai-telegram-bot-agent-smart-assistant-content-summarizer-4457", - "matches": 9, - "matched_keywords": [ - "day", - "made", - "different", - "product", - "summarize", - "post", - "summary", - "build", - "schedule" - ] - } - }, - { - "prompt_file": "form-to-hubspot.json", - "prompt": "Create a form that collects: name, email, company, and interest level (dropdown: starter, professional, enterprise). When submitted, create a new contact in HubSpot with firstname, lastname (split fro", - "keywords": [ - "form", - "collects", - "email", - "company", - "interest", - "level", - "dropdown", - "starter", - "professional", - "enterprise", - "new", - "contact", - "hubspot", - "firstname", - "lastname", - "split", - "custom", - "property", - "confirmation", - "sendgrid", - "address", - "subject", - "reaching", - "body", - "mention", - "later", - "build", - "trigger", - "crm" - ], - "matched": true, - "top_match": { - "slug": "scrape-google-maps-business-leads-with-apify-gpt-4-email-ext-10640", - "matches": 13, - "matched_keywords": [ - "form", - "email", - "company", - "professional", - "contact", - "hubspot", - "split", - "custom", - "confirmation", - "address", - "build", - "trigger", - "crm" - ] - } - }, - { - "prompt_file": "github-notion-sync.json", - "prompt": "Every day, fetch all open GitHub issues from repository 'acme-corp/backend' that have the label 'bug'. For each issue, create a page in a Notion database (database ID: 'a1b2c3d4e5f6789012345678abcdef0", - "keywords": [ - "day", - "open", - "github", - "issues", - "repository", - "acme-corp", - "backend", - "label", - "bug", - "issue", - "page", - "notion", - "database", - "a1b2c3d4e5f6789012345678abcdef01", - "properties", - "title", - "html", - "created", - "date", - "assignee", - "login", - "unassigned", - "status", - "directly", - "repos", - "labels", - "state", - "bearer", - "token", - "authorization", - "header", - "later", - "build", - "schedule", - "sync" - ], - "matched": true, - "top_match": { - "slug": "extract-invoice-data-from-email-to-google-sheets-using-gpt-4-4376", - "matches": 11, - "matched_keywords": [ - "day", - "open", - "issues", - "label", - "issue", - "page", - "database", - "date", - "labels", - "header", - "schedule" - ] - } - }, - { - "prompt_file": "linear-bq-leaderboard.json", - "prompt": "Every two weeks I want to check the amount of n8n usage and bug reporting that the team has done and produce a leaderboard that then gets posted to Slack (channel ID: D034WT7G4CW).\n\nHere are the users", - "keywords": [ - "weeks", - "want", - "check", - "amount", - "n8n", - "usage", - "bug", - "reporting", - "team", - "produce", - "leaderboard", - "posted", - "slack", - "channel", - "d034wt7g4cw", - "here", - "users", - "david", - "roberts", - "arens", - "niklas", - "hatje", - "example", - "last", - "jonathan", - "clift", - "tickets", - "execs", - "hours", - "fabian", - "puehringer", - "tuukka", - "kantola", - "linear", - "created", - "manual", - "registered", - "accounts", - "ordered", - "number", - "desc", - "bugs", - "user", - "reported", - "query", - "issues", - "any", - "label", - "case-sensitive", - "matched", - "connect", - "bigquery", - "something", - "similar", - "following", - "settings", - "select", - "timestamp", - "start", - "cutoff", - "end", - "unnest", - "struct", - "string", - "exec", - "trunc", - "hour", - "instance", - "status", - "rudder", - "schema", - "finished", - "inner", - "join", - "cross", - "where", - "between", - "union", - "summary", - "count", - "distinct", - "instances", - "group", - "later", - "build", - "schedule" - ], - "matched": true, - "top_match": { - "slug": "extract-invoice-data-from-email-to-google-sheets-using-gpt-4-4376", - "matches": 29, - "matched_keywords": [ - "check", - "amount", - "n8n", - "reporting", - "team", - "channel", - "hours", - "linear", - "manual", - "accounts", - "number", - "desc", - "issues", - "any", - "label", - "connect", - "similar", - "settings", - "select", - "timestamp", - "end", - "struct", - "hour", - "instance", - "join", - "cross", - "count", - "group", - "schedule" - ] - } - }, - { - "prompt_file": "notification-router.json", - "prompt": "Create a workflow that receives webhook notifications with a JSON body containing 'level' (high, medium, or low), 'title', and 'message'. Route them based on level: high priority goes to Microsoft Tea", - "keywords": [ - "receives", - "webhook", - "notifications", - "json", - "body", - "containing", - "level", - "high", - "medium", - "low", - "title", - "route", - "based", - "priority", - "goes", - "microsoft", - "teams", - "team", - "9b4c3a2f-1d8e-4f5b-a6c7-8e9f0b1d2c3a", - "channel", - "a1b2c3d4e5f6", - "thread", - "tacv2", - "slack", - "gmail", - "alerts", - "ourcompany", - "notification", - "include", - "payload", - "later", - "build", - "switch", - "routing" - ], - "matched": true, - "top_match": { - "slug": "extract-invoice-data-from-email-to-google-sheets-using-gpt-4-4376", - "matches": 13, - "matched_keywords": [ - "json", - "body", - "level", - "high", - "medium", - "low", - "based", - "microsoft", - "teams", - "team", - "channel", - "gmail", - "include" - ] - } - }, - { - "prompt_file": "rest-api-data-pipeline.json", - "prompt": "Fetch the latest posts from the JSONPlaceholder API (GET https://jsonplaceholder.typicode.com/posts). Filter out any posts where the title contains the word 'qui'. Then post a summary message to a Sla", - "keywords": [ - "latest", - "posts", - "jsonplaceholder", - "typicode", - "filter", - "any", - "where", - "title", - "contains", - "word", - "qui", - "post", - "summary", - "slack", - "channel", - "called", - "api-digest", - "says", - "many", - "remain", - "lists", - "titles", - "later", - "build", - "transformation", - "schedule" - ], - "matched": true, - "top_match": { - "slug": "scrape-linkedin-job-listings-for-hiring-signals-prospecting--3580", - "matches": 10, - "matched_keywords": [ - "posts", - "filter", - "any", - "title", - "word", - "qui", - "post", - "lists", - "titles", - "build" - ] - } - }, - { - "prompt_file": "set-edit-fields-contract.json", - "prompt": "Every day, fetch one post from the JSONPlaceholder API (GET https://jsonplaceholder.typicode.com/posts/1). Then use an Edit Fields (Set) node, not a Code node, to add a field called caption from the p", - "keywords": [ - "day", - "post", - "jsonplaceholder", - "typicode", - "posts", - "edit", - "not", - "code", - "called", - "caption", - "title", - "source", - "while", - "preserving", - "original", - "later", - "build", - "schedule", - "transformation" - ], - "matched": true, - "top_match": { - "slug": "scrape-linkedin-job-listings-for-hiring-signals-prospecting--3580", - "matches": 8, - "matched_keywords": ["day", "post", "posts", "edit", "not", "code", "title", "build"] - } - }, - { - "prompt_file": "telegram-chatbot-memory-session.json", - "prompt": "Build a Telegram chatbot workflow for a family assistant. It should receive Telegram messages, answer with an AI Agent using an OpenAI chat model, keep short-term conversation memory scoped separately", - "keywords": [ - "build", - "telegram", - "chatbot", - "family", - "assistant", - "receive", - "answer", - "agent", - "openai", - "chat", - "model", - "keep", - "short-term", - "conversation", - "memory", - "scoped", - "separately", - "back", - "same", - "later", - "expressions" - ], - "matched": true, - "top_match": { - "slug": "build-a-voice-ai-chatbot-with-elevenlabs-and-infranodus-know-4484", - "matches": 13, - "matched_keywords": [ - "build", - "telegram", - "chatbot", - "receive", - "answer", - "agent", - "openai", - "chat", - "keep", - "conversation", - "memory", - "back", - "same" - ] - } - }, - { - "prompt_file": "weather-alert.json", - "prompt": "Every day at 8am, check the weather in Berlin using the OpenMeteo API and send me an email to david@thedavid.co.uk using the gmail node if it's going to rain", - "keywords": [ - "day", - "8am", - "check", - "weather", - "berlin", - "openmeteo", - "email", - "david", - "thedavid", - "gmail", - "going", - "rain", - "build", - "schedule", - "conditional" - ], - "matched": true, - "top_match": { - "slug": "extract-invoice-data-from-email-to-google-sheets-using-gpt-4-4376", - "matches": 7, - "matched_keywords": ["day", "check", "email", "gmail", "going", "rain", "schedule"] - } - }, - { - "prompt_file": "weather-monitoring.json", - "prompt": "Every hour, check the current weather for London, New York, and Tokyo using the OpenWeatherMap API. Use 3 separate HTTP Request nodes, one per city. If any city has a temperature above 30°C, send a Te", - "keywords": [ - "hour", - "check", - "current", - "weather", - "london", - "new", - "york", - "tokyo", - "openweathermap", - "separate", - "per", - "city", - "any", - "temperature", - "above", - "telegram", - "alert", - "chat", - "-1001234567890", - "listing", - "hot", - "cities", - "log", - "readings", - "airtable", - "table", - "base", - "appk2xgfgnoirl2gt", - "tbl8xk3np5mq7rs9w", - "columns", - "humidity", - "timestamp", - "later", - "build", - "schedule", - "conditional", - "multi" - ], - "matched": true, - "top_match": { - "slug": "extract-invoice-data-from-email-to-google-sheets-using-gpt-4-4376", - "matches": 14, - "matched_keywords": [ - "hour", - "check", - "current", - "new", - "per", - "city", - "any", - "chat", - "log", - "table", - "base", - "timestamp", - "schedule", - "multi" - ] - } - }, - { - "prompt_file": "workflow-data-table.json", - "prompt": "I want you to build a workflow that will read n8n workflow databases and extract certain information and then populate that information in a data table called 'workflows'.\n\nThe schema of the data tabl", - "keywords": [ - "want", - "you", - "build", - "read", - "n8n", - "databases", - "extract", - "certain", - "information", - "populate", - "table", - "called", - "schema", - "follows", - "instanceid", - "workflowid", - "workflowname", - "tags", - "run", - "multiple", - "times", - "update", - "current", - "rows", - "rather", - "than", - "creating", - "dupes", - "instance", - "wonderman", - "users", - "cloud", - "later", - "schedule" - ], - "matched": true, - "top_match": { - "slug": "extract-invoice-data-from-email-to-google-sheets-using-gpt-4-4376", - "matches": 15, - "matched_keywords": [ - "you", - "read", - "n8n", - "extract", - "certain", - "information", - "populate", - "table", - "multiple", - "times", - "update", - "current", - "instance", - "cloud", - "schedule" - ] - } - } - ] -} diff --git a/packages/@n8n/workflow-sdk/examples/manifest.json b/packages/@n8n/workflow-sdk/examples/manifest.json deleted file mode 100644 index ad7ca0d0403..00000000000 --- a/packages/@n8n/workflow-sdk/examples/manifest.json +++ /dev/null @@ -1,3491 +0,0 @@ -{ - "generatedAt": "2026-05-07T14:34:49.852Z", - "workflows": [ - { - "id": 6270, - "slug": "build-your-first-ai-agent-6270", - "name": "Build Your First AI Agent", - "description": "## How it works\n\nThis template launches your very first **AI Agent** —an AI-powered chatbot that can do more than just talk— it can take action using tools.\n\nThink of an AI Agent as a smart assistant, and the tools are the apps on its phone. By connecting it to other nodes, you give your agent the ability to interact with real-world data and services, like checking the weather, fetching news, or even sending emails on your behalf.\n\nThis workflow is designed to be the perfect starting point:\n* **The Chat Interface:** A `Chat Trigger` node provides a simple, clean interface for you to talk to your agent.\n* **The Brains:** The `AI Agent` node receives your messages, intelligently decides which tool to use (if any), and formulates a helpful response. Its personality and instructions are fully customizable in the \"System Message\".\n* **The Language Model:** It uses **Google Gemini** to power its reasoning and conversation skills.\n* **The Tools:** It comes pre-equipped with two tools to demonstrate its capabilities:\n 1. **Get Weather:** Fetches real-time weather forecasts.\n 2. **Get News:** Reads any RSS feed to get the latest headlines.\n* **The Memory:** A `Conversation Memory` node allows the agent to remember the last few messages, enabling natural, follow-up conversations.\n\n### Set up steps\n\n**Setup time: ~2 minutes**\n\nYou only need one thing to get started: a free Google AI API key.\n\n1. **Get Your Google AI API Key:**\n * Visit Google AI Studio at [aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey).\n * Click **\"Create API key in new project\"** and copy the key that appears.\n\n2. **Add Your Credential in n8n:**\n * On the workflow canvas, go to the **`Connect your model`** (Google Gemini) node.\n * Click the **Credential** dropdown and select **`+ Create New Credential`**.\n * Paste your API key into the **API Key** field and click **Save**.\n\n3. **Start Chatting!**\n * Go to the **`Example Chat`** node.\n * Click the **\"Open Chat\"** button in its parameter panel.\n * Try asking it one of the example questions, like: *\"What's the weather in Paris?\"* or *\"Get me the latest tech news.\"*\n\nThat's it! You now have a fully functional AI Agent. Try adding more tools (like Gmail or Google Calendar) to make it even more powerful.", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.rssFeedReadTool", - "n8n-nodes-base.httpRequestTool", - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.chatTrigger", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "@n8n/n8n-nodes-langchain.lmChatGoogleGemini" - ], - "tags": ["trigger:chatTrigger", "ai", "integration:langchain"], - "triggerType": "chatTrigger", - "hasAI": true, - "score": 95, - "scoreBreakdown": { - "traction": 1, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/6270", - "author": "lucaspeyrin", - "success": true - }, - { - "id": 8237, - "slug": "personal-life-manager-with-telegram-google-services-voice-en-8237", - "name": "Personal Life Manager with Telegram, Google Services & Voice-Enabled AI", - "description": "**How it works:**\nThis project teaches you to create a personal AI assistant named Jackie that operates through Telegram. Jackie can summarize unread emails, check calendar events, manage Google Tasks, and handle both voice and text interactions. The assistant provides a comprehensive digital life management solution accessible via Telegram messaging.\n\n**Key Features:** \n- Supports hands-free voice interaction\n- Maintains conversation memory\n- Integrates with major Google services\n- Provides personalized assistance for email management, scheduling, and task organization\n\n**Step-by-step:**\nTelegram Trigger: \nThe workflow starts with a Telegram trigger that listens for incoming message events. The system determines if the incoming message is voice or text input.\n\nVoice Processing: \nIf a voice message is received, the workflow retrieves the voice file from Telegram and uses OpenAI's transcription API to convert speech to text.\n\nAI Assistant: The processed text (whether original text or transcribed voice) is passed to Jackie, the AI assistant powered by OpenRouter's language model.\n\n**Tools Integration:** \nJackie is equipped with several productivity tools:\n\nGet Email: Uses Gmail API to fetch unread emails from the inbox with sender, date, subject, and summary information\n\nGoogle Calendar: Retrieves calendar events for specified dates, filtering out irrelevant future events\n\nGoogle Tasks: Both creates new tasks and retrieves existing tasks from Google Tasks lists\n\n**API Keys Required:**\n- Telegram Bot API: Create a bot via @BotFather on Telegram to get your bot token\n- OpenAI API: Required for voice-to-text transcription \n- OpenRouter API: Powers the AI language model responses \n- Google OAuth2: Needed for Gmail, Google Calendar, and Google Tasks integration\n\n\n**Response Generation:**\nThe AI formulates intelligent responses based on the gathered information, current date context, and conversation history, then sends the response back to the user via Telegram in Markdown format.\n", - "nodes": [ - "n8n-nodes-base.googleCalendarTool", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "n8n-nodes-base.gmailTool", - "n8n-nodes-base.telegramTrigger", - "n8n-nodes-base.telegram", - "n8n-nodes-base.if", - "n8n-nodes-base.set", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.lmChatOpenRouter", - "n8n-nodes-base.googleTasksTool", - "@n8n/n8n-nodes-langchain.openAi", - "@n8n/n8n-nodes-langchain.agent" - ], - "tags": ["trigger:telegram", "ai", "integration:langchain"], - "triggerType": "telegram", - "hasAI": true, - "score": 90.95, - "scoreBreakdown": { - "traction": 0.851, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.786 - }, - "source": "https://n8n.io/workflows/8237", - "author": "derekcheungsa", - "success": true - }, - { - "id": 2753, - "slug": "rag-chatbot-for-company-documents-using-google-drive-and-gem-2753", - "name": "RAG Chatbot for Company Documents using Google Drive and Gemini", - "description": "This workflow implements a Retrieval Augmented Generation (RAG) chatbot that answers employee questions based on company documents stored in Google Drive. It automatically indexes new or updated documents in a Pinecone vector database, allowing the chatbot to provide accurate and up-to-date information. The workflow uses Google's Gemini AI for both embeddings and response generation.\n\n## How it works\nThe workflow uses two `Google Drive Trigger` nodes: one for detecting new files added to a specified Google Drive folder, and another for detecting file updates in that same folder.\n1. Automated Indexing: When a new or updated document is detected\n2. The `Google Drive` node downloads the file.\n3. The `Default Data Loader` node loads the document content.\n4. The `Recursive Character Text Splitter` node breaks the document into smaller text chunks.\n5. The Embeddings Google Gemini node generates embeddings for each text chunk using the text-embedding-004 model.\n6. The `Pinecone Vector Store` node indexes the text chunks and their embeddings in a specified Pinecone index.\n7.The Chat Trigger node receives user questions through a chat interface. The user's question is passed to an AI Agent node.\n8. The `AI Agent` node uses a `Vector Store Tool` node, linked to a Pinecone Vector Store node in query mode, to retrieve relevant text chunks from Pinecone based on the user's question.\n9. The AI Agent sends the retrieved information and the user's question to the Google Gemini Chat Model (gemini-pro).\n10. The `Google Gemini Chat Model` generates a comprehensive and informative answer based on the retrieved documents.\n11. A `Window Buffer Memory` node connected to the AI Agent provides short-term memory, allowing for more natural and context-aware conversations.\n\n## Set up steps\n\n1. Google Cloud Project and Vertex AI API:\n* Create a Google Cloud project.\n* Enable the Vertex AI API for your project.\n2. Google AI API Key:\n* Obtain a Google AI API key from Google AI Studio.\n3. Pinecone Account:\n* Create a free account on the Pinecone website.\nObtain your API key from your Pinecone dashboard.\n* Create an index named company-files in your Pinecone project.\n4. Google Drive:\n* Create a dedicated folder in your Google Drive where company documents will be stored.\n5. Credentials in n8n: Configure credentials in your n8n environment for:\n* Google Drive OAuth2\n* Google Gemini(PaLM) Api (using your Google AI API key)\n* Pinecone API (using your Pinecone API key)\n5. Import the Workflow:\n* Import this workflow into your n8n instance.\n6. Configure the Workflow:\n* Update both Google Drive Trigger nodes to watch the specific folder you created in your Google Drive.\n* Configure the Pinecone Vector Store nodes to use your company-files index.", - "nodes": [ - "@n8n/n8n-nodes-langchain.vectorStorePinecone", - "@n8n/n8n-nodes-langchain.embeddingsGoogleGemini", - "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", - "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter", - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.toolVectorStore", - "n8n-nodes-base.googleDrive", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.googleDriveTrigger", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "@n8n/n8n-nodes-langchain.chatTrigger", - "@n8n/n8n-nodes-langchain.lmChatGoogleGemini" - ], - "tags": ["trigger:other", "ai", "integration:langchain"], - "triggerType": "other", - "hasAI": true, - "score": 90.35, - "scoreBreakdown": { - "traction": 0.834, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.733 - }, - "source": "https://n8n.io/workflows/2753", - "author": "mihailtd", - "success": true - }, - { - "id": 4846, - "slug": "generate-ai-videos-with-google-veo3-save-to-google-drive-and-4846", - "name": "Generate AI Videos with Google Veo3, Save to Google Drive and Upload to YouTube", - "description": "This workflow allows users to **generate AI videos** using **Google Veo3**, save them to **Google Drive**, generate optimized YouTube titles with GPT-4o, and **automatically upload them to YouTube** with [Upload-Post](https://www.upload-post.com/?linkId=lp_144414&sourceId=n3witalia&tenantId=upload-post-app). The entire process is triggered from a Google Sheet that acts as the central interface for input and output.\n\nIT automates video creation, uploading, and tracking, ensuring seamless integration between Google Sheets, Google Drive, Google Veo3, and YouTube.\n\n\n---\n\n### Benefits of this Workflow\n\n* **💡 No Code Interface**: Trigger and control the video production pipeline from a simple Google Sheet.\n* **⚙️ Full Automation**: Once set up, the entire video generation and publishing process runs hands-free.\n* **🧠 AI-Powered Creativity**:\n\n * Generates engaging YouTube titles using GPT-4o.\n * Leverages advanced generative video AI from Google Veo3.\n* **📁 Cloud Storage & Backup**: Stores all generated videos on Google Drive for safekeeping.\n* **📈 YouTube Ready**: Automatically uploads to YouTube with correct metadata, saving time and boosting visibility.\n* **🧪 Scalable**: Designed to process multiple video prompts by looping through new entries in Google Sheets.\n* **🔒 API-First**: Utilizes secure API-based communication for all services.\n\n---\n\n### **How It Works** \n1. **Trigger**: The workflow can be started manually (\"When clicking ‘Test workflow’\") or scheduled (\"Schedule Trigger\") to run at regular intervals (e.g., every 5 minutes). \n2. **Fetch Data**: The \"Get new video\" node retrieves unfilled video requests from a Google Sheet (rows where the \"VIDEO\" column is empty). \n3. **Video Creation**: \n - The \"Set data\" node formats the prompt and duration from the Google Sheet. \n - The \"Create Video\" node sends a request to the Fal.run API (Google Veo3) to generate a video based on the prompt. \n4. **Status Check**: \n - The \"Wait 60 sec.\" node pauses execution for 60 seconds. \n - The \"Get status\" node checks the video generation status. If the status is \"COMPLETED,\" the workflow proceeds; otherwise, it waits again. \n5. **Video Processing**: \n - The \"Get Url Video\" node fetches the video URL. \n - The \"Generate title\" node uses OpenAI (GPT-4.1) to create an SEO-optimized YouTube title. \n - The \"Get File Video\" node downloads the video file. \n6. **Upload & Update**: \n - The \"Upload Video\" node saves the video to Google Drive. \n - The \"HTTP Request\" node uploads the video to YouTube via the Upload-Post API. \n - The \"Update Youtube URL\" and \"Update result\" nodes update the Google Sheet with the video URL and YouTube link. \n\n---\n\n### **Set Up Steps** \n1. **Google Sheet Setup**: \n - Create a Google Sheet with columns: **PROMPT**, **DURATION**, **VIDEO**, and **YOUTUBE_URL**. \n - Share the Sheet link in the \"Get new video\" node. \n\n2. **API Keys**: \n - Obtain a Fal.run API key (for Veo3) and set it in the \"Create Video\" node (Header: `Authorization: Key YOURAPIKEY`). \n - Get an Upload-Post API key (for YouTube uploads) and configure the \"HTTP Request\" node (Header: `Authorization: Apikey YOUR_API_KEY`). \n\n3. **YouTube Upload Configuration**: \n - Replace `YOUR_USERNAME` in the \"HTTP Request\" node with your Upload-Post profile name. \n\n4. **Schedule Trigger**: \n - Configure the \"Schedule Trigger\" node to run periodically (e.g., every 5 minutes). \n\n---\n\n👉 [Subscribe to my new **YouTube channel**](https://youtube.com/@n3witalia). Here I’ll share videos and Shorts with practical tutorials and **FREE templates for n8n**.\n\n[![image](https://n3wstorage.b-cdn.net/n3witalia/youtube-n8n-cover.jpg)](https://youtube.com/@n3witalia)\n\n\n---\n\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/).", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.wait", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.if", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.set", - "n8n-nodes-base.googleDrive", - "@n8n/n8n-nodes-langchain.openAi" - ], - "tags": ["trigger:manual", "ai", "integration:httpRequest"], - "triggerType": "manual", - "hasAI": true, - "score": 90.19, - "scoreBreakdown": { - "traction": 0.859, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.6 - }, - "source": "https://n8n.io/workflows/4846", - "author": "n3witalia", - "success": true - }, - { - "id": 4352, - "slug": "ai-powered-multi-social-media-post-automation-google-trends--4352", - "name": "AI-Powered Multi-Social Media Post Automation: Google Trends & Perplexity AI ", - "description": "![Screenshot 20250524 013951.png](fileId:1366)\n## Overview\nThis comprehensive n8n workflow automatically transforms trending Google search queries into engaging LinkedIn posts using AI. The system runs autonomously, discovering viral topics, researching content, and publishing professionally formatted posts to grow your social media presence.\n\n## Workflow Description\n**Automate your entire social media content pipeline** - from trend discovery to publication. This workflow monitors Google Trends, selects high-potential topics, creates human-like content using advanced AI, and publishes across multiple social platforms with built-in tracking.\n\n## Key Features\n- **Automated Trend Discovery**: Pulls trending topics from Google Trends API with customizable filters\n- **Intelligent Topic Selection**: AI chooses the most relevant trending topic for your niche\n- **Multi-AI Content Generation**: Combines Perplexity for research and OpenAI for content curation\n- **Human-Like Writing**: Advanced prompts eliminate AI detection markers\n- **LinkedIn Optimization**: Proper formatting with Unicode characters, emojis, and engagement hooks\n- **Multi-Platform Support**: Ready for LinkedIn, Twitter/X, and Facebook posting\n- **Automated Scheduling**: Configurable posting times (default: 6 AM & 6 PM daily)\n- **Performance Tracking**: Automatic logging to Google Sheets with timestamps and metrics\n- **Error Handling**: Built-in delays and retry mechanisms for API stability\n\n## Technical Implementation\n\n### Workflow Architecture\n1. **Schedule Trigger**: Automated execution at specified intervals\n2. **Google Trends API**: Fetches trending search queries with geographical filtering\n3. **Data Processing**: JavaScript code node filters high-volume keywords (30+ search volume)\n4. **Topic Selection**: OpenAI GPT-3.5 evaluates and selects optimal trending topic\n5. **Content Research**: Perplexity AI researches selected topic for current information\n6. **Content Generation**: Advanced prompt engineering creates LinkedIn-optimized posts\n7. **Content Distribution**: Multi-platform posting with platform-specific formatting\n8. **Analytics Tracking**: Google Sheets integration for performance monitoring\n\n### Node Breakdown\n- **Schedule Trigger**: Configurable timing for automated execution\n- **HTTP Request (Google Trends)**: SerpAPI integration for trend data\n- **Set Node**: Structures trending data for processing\n- **Code Node**: JavaScript filtering for high-volume keywords\n- **OpenAI Node**: Intelligent topic selection based on relevance and trend strength\n- **HTTP Request (Perplexity)**: Advanced AI research with anti-detection prompts\n- **Wait Node**: Rate limiting and API respect\n- **Split Out**: Prepares content for multi-platform distribution\n- **LinkedIn Node**: Authenticated posting with community management\n- **Google Sheets Node**: Automated tracking and analytics\n- **Social Media Nodes**: Twitter/X, LinkedIn and Facebook ready for activation\n\n## Use Cases\n- **Content Creators**: Maintain consistent posting schedules with trending content\n- **Marketing Agencies**: Scale content creation across multiple client accounts\n- **Business Development**: Build thought leadership with timely industry insights\n- **Personal Branding**: Establish authority by commenting on trending topics\n- **SEO Professionals**: Create content around high-search-volume keywords\n\n## Configuration Requirements\n\n### API Integrations\n- **SerpAPI**: Google Trends data access\n- **Perplexity AI**: Advanced content research capabilities\n- **OpenAI**: Content curation and topic selection\n- **LinkedIn Community Management API**: Professional posting access\n- **Google Sheets API**: Analytics and tracking\n\n### Authentication Setup\n- LinkedIn OAuth2 community management credentials\n- Google Sheets OAuth2 integration\n- HTTP header authentication for AI services\n\n## Customization Options\n- **Industry Targeting**: Modify prompts for specific business verticals\n- **Posting Schedule**: Adjust timing based on audience activity\n- **Content Tone**: Customize voice and style through prompt engineering\n- **Platform Selection**: Enable/disable specific social media channels\n- **Trend Filtering**: Adjust search volume thresholds and geographic targeting\n- **Content Length**: Modify character limits for different platforms\n\n## Advanced Features\n- **Anti-AI Detection**: Sophisticated prompts create human-like content\n- **Rate Limit Management**: Built-in delays prevent API throttling\n- **Error Recovery**: Robust error handling with retry mechanisms\n- **Content Deduplication**: Prevents posting duplicate content\n- **Engagement Optimization**: LinkedIn-specific formatting for maximum reach\n\n## Performance Metrics\n- **Time Savings**: Eliminates 10+ hours of weekly content creation\n- **Consistency**: Maintains regular posting schedule without manual intervention\n- **Relevance**: Content always based on current trending topics\n- **Engagement**: Optimized formatting increases social media interaction\n- **Scalability**: Single workflow manages multiple platform posting\n\n## Installation Notes\n- Import JSON workflow file into n8n instance\n- Configure all required API credentials\n- Set up [Google Sheets](Google Sheets) tracking document\n- Test workflow execution with manual trigger\n- Enable schedule trigger for automated operation\n\n## Best Practices\n- Monitor API usage to stay within rate limits\n- Regularly update prompts based on content performance\n- Review and adjust trending topic filters for your niche\n- Maintain backup of workflow configuration\n- Test content output before enabling automation\n\n## Support & Updates\n- Comprehensive setup documentation included\n- Configuration troubleshooting guide provided\n- Regular workflow updates for API changes\n- Community support through n8n forums\n\n## Tags\n`social-media` `content-automation` `linkedin` `ai-generation` `google-trends` `perplexity` `openai` `marketing` `trend-analysis` `content-creation`\n\n## Compatibility\n- n8n Version: 1.0+\n- Node Requirements: Standard n8n installation\n- External Dependencies: API access to listed services\n- Hosting: Compatible with cloud and self-hosted n8n instances", - "nodes": [ - "n8n-nodes-base.twitter", - "n8n-nodes-base.facebookGraphApi", - "n8n-nodes-base.linkedIn", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.set", - "n8n-nodes-base.code", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.wait" - ], - "tags": ["trigger:schedule", "ai", "integration:httpRequest"], - "triggerType": "schedule", - "hasAI": true, - "score": 89.9, - "scoreBreakdown": { - "traction": 0.766, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.917 - }, - "source": "https://n8n.io/workflows/4352", - "author": "dominixai", - "success": true - }, - { - "id": 5148, - "slug": "local-chatbot-with-retrieval-augmented-generation-rag-5148", - "name": "Local Chatbot with Retrieval Augmented Generation (RAG)", - "description": "## Build a 100% local RAG with n8n, Ollama and Qdrant. This agent uses a semantic database (Qdrant) to answer questions about PDF files.\n\n## Tutorial\n![thumbnail.png](fileId:1589)\n[Click here to view the YouTube Tutorial](https://youtu.be/maZ_fF57yhE)\n\n## How it works\nBuild a chatbot that answers based on documents you provide it (Retrieval Augmented Generation). You can upload as many PDF files as you want to the Qdrant database. The chatbot will use its retrieval tool to fetch the chunks and use them to answer questions.\n\n## Installation\n1. Install n8n + Ollama + Qdrant using the [Self-hosted AI starter kit](https://github.com/n8n-io/self-hosted-ai-starter-kit)\n2. Make sure to install Llama 3.2 and mxbai-embed-large as embeddings model.\n\n## How to use it\n1. First run the \"Data Ingestion\" part and upload as many PDF files as you want\n2. Run the Chatbot and start asking questions about the documents you uploaded\n", - "nodes": [ - "n8n-nodes-base.formTrigger", - "@n8n/n8n-nodes-langchain.vectorStoreQdrant", - "@n8n/n8n-nodes-langchain.embeddingsOllama", - "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", - "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.chatTrigger", - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.lmChatOllama", - "@n8n/n8n-nodes-langchain.memoryBufferWindow" - ], - "tags": ["trigger:formTrigger", "ai", "integration:langchain"], - "triggerType": "formTrigger", - "hasAI": true, - "score": 89.7, - "scoreBreakdown": { - "traction": 0.781, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.818 - }, - "source": "https://n8n.io/workflows/5148", - "author": "thomasjanssen-tech", - "success": true - }, - { - "id": 4966, - "slug": "customer-support-whatsapp-bot-with-google-docs-knowledge-bas-4966", - "name": "Customer Support WhatsApp Bot with Google Docs Knowledge Base and Gemini AI", - "description": "* Document-Aware WhatsApp AI Bot for Customer Support\n\n* Google Docs-Powered WhatsApp Support Agent\n\n* 24/7 WhatsApp AI Assistant with Live Knowledge from Google Docs\n\n# 📝Description Template\n\n## Smart WhatsApp AI Assistant Using Google Docs\n\nHelp customers instantly on WhatsApp using a smart AI assistant that reads your company’s internal knowledge from a Google Doc in real time. Built for clubs, restaurants, agencies, or any business where clients ask questions based on a policy, FAQ, or services document.\n\n# ⚙️ How it works\n\n- Users send free-form questions to your WhatsApp Business number (e.g. “What are the gym rules?” or “Are you open today?”)\n\n- The bot automatically reads your company’s internal Google Doc (policy, schedule, etc.)\n\n- It merges the document content with today’s date and the user’s question to craft a custom AI prompt\n\n- The AI (Gemini or ChatGPT) then replies back on WhatsApp using natural, helpful language\n\n- All conversations are logged to Google Sheets for reporting or audit\n\n> 💡Bonus: The AI even understands dates inside the document and compares them to today’s date — e.g. if your document says “Closed May 25 for 30 days,” it will say “We're currently closed until June 24.\n\n# 🧰 Set up steps\n\n1. Connect your WhatsApp Cloud API account (Meta)\n\n2. Add your Google account and grant access to the Doc containing your company info\n\n3. Choose your AI model (ChatGPT/OpenAI or Gemini)\n\n4. Paste your document ID into the Google Docs node\n\n\n\n5. Connect your WhatsApp webhook to Meta (only takes 5 minutes)\n\n6. Done — start receiving and answering customer questions!\n\n> 📄 Works best with free-tier OpenAI/Gemini, Google Docs, and Meta's Cloud API (no phone required). Everything is modular, extensible, and low-code.\n\n# 🔄 Customization Tips\n\n* Change the Google Doc anytime to update answers — no retraining needed\n\n* Add your logo and business name in the AI agent’s “System Prompt”\n\n* Add fallback routes like “Escalate to human” if the bot can't help\n\n* Clone for multiple brands by duplicating the workflow and swapping in new docs\n\n\n🤝 Need Help Setting It Up?\n\nIf you'd like help connecting your WhatsApp Business API, setting up Google Docs access, or customizing this AI assistant for your business or clients…\n\n📩 I offer setup, branding, and customization services:\n\n\n\n\n\nWhatsApp Cloud API setup & verification\n\n\n\nGoogle OAuth & Doc structure guidance\n\n\n\nAI model configuration (OpenAI / Gemini)\n\n\n\nBranding & prompt tone customization\n\n\n\nLogging, reporting, and escalation logic\n\nJust send a message via:\n\n\n\n\n\nEmail: tharwat.elsayed2000@gmail.com\n\n\n\nWhatsApp: +20 106 180 3236", - "nodes": [ - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.code", - "n8n-nodes-base.if", - "n8n-nodes-base.whatsApp", - "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", - "n8n-nodes-base.whatsAppTrigger", - "n8n-nodes-base.aiTransform", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.dateTime", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.googleDocs", - "@n8n/n8n-nodes-langchain.memoryBufferWindow" - ], - "tags": ["trigger:other", "ai", "integration:langchain"], - "triggerType": "other", - "hasAI": true, - "score": 89.7, - "scoreBreakdown": { - "traction": 0.774, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.846 - }, - "source": "https://n8n.io/workflows/4966", - "author": "tharwatelsayed", - "success": true - }, - { - "id": 5626, - "slug": "free-ai-image-generator-n8n-automation-workflow-with-gemini--5626", - "name": "Free AI Image Generator - n8n Automation Workflow with Gemini/ChatGPT", - "description": "### This n8n template demonstrates how to use AI to generate custom images from scratch - fully automated, prompt-driven, and ready to deploy at scale.\n\nUse cases are many: You can use it for marketing visuals, character art, digital posters, storyboards, or even daily image generation for your personal purposes.\n\n## How It Works\n- The flow is triggered by a chat message in N8N or via Telegram. The default image size is 1080 x 1920 pixels. To use a different size, update the values in the **“Fields - Set Values”** node before triggering the workflow.\n- The input is parsed into a clean, structured prompt using a multi-step transformation process.\n- Our AI Agent sends the final prompt to Google Gemini’s image model for generation (you can also integrate with OpenAI or other chat models).\n- The raw image data created by the AI Agent will be run through a number of codes to make sure it's feasible for your preview if needed and downloading.\n- Then, we use an HTTP node to fetch the result so you can preview the image.\n- You can send it back to the chat message in N8N or Telegram, or save it locally to your disk.\n\n## How To Use\n- Download the workflow package.\n- Import the package into your N8N interface.\n- Set up the credentials in the following nodes for tool access and usability: **\"Telegram Trigger\"**; **\"AI Agent - Create Image From Prompt\"**; **\"Telegram Response\"** or **\"Save Image To Disk\"** (based on your wish).\n- Activate the **\"Telegram Response\"** OR **\"Save Image To Disk\"** node to specify where you want to save your image later.\n- Open the chat interface (via N8N or Telegram).\n- Type your image prompt or detailed descriptions and send.\n- Wait for the process to run and finish in a few seconds.\n- Check the result in your desired saving location.\n\n## Requirements\n- Google Gemini account with image generation access.\n- Telegram bot access and chat setup (optional).\n- Connection to local storage (optional).\n\n## How To Customize\n- We’re setting the default image size to 1080 x 1920 pixels and the default image model to \"flux\". You can customize both of these values in the **“Fields – Set Values”** node. Supported image model options include: \"flux\", \"kontext\", \"turbo\", and \"gptimage\".\n- In the **“AI Agent – Create Image From Prompt”** node, you can also change the AI chat model. By default, it uses Google Gemini, but you can easily replace it with OpenAI ChatGPT, Microsoft AI Copilot, or any other compatible provider.\n\n## Need Help?\nJoin our community on different platforms for support, inspiration and tips from others.\n\nWebsite: https://www.agentcircle.ai/\nEtsy: https://www.etsy.com/shop/AgentCircle\nGumroad: http://agentcircle.gumroad.com/\nDiscord Global: https://discord.gg/d8SkCzKwnP\nFB Page Global: https://www.facebook.com/agentcircle/\nFB Group Global: https://www.facebook.com/groups/aiagentcircle/\nX: https://x.com/agent_circle\nYouTube: https://www.youtube.com/@agentcircle\nLinkedIn: https://www.linkedin.com/company/agentcircle", - "nodes": [ - "n8n-nodes-base.set", - "@n8n/n8n-nodes-langchain.chatTrigger", - "n8n-nodes-base.code", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.telegramTrigger", - "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.telegram", - "n8n-nodes-base.readWriteFile" - ], - "tags": ["trigger:chatTrigger", "ai", "integration:code"], - "triggerType": "chatTrigger", - "hasAI": true, - "score": 89.61, - "scoreBreakdown": { - "traction": 0.776, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.818 - }, - "source": "https://n8n.io/workflows/5626", - "author": "agentcircle", - "success": true - }, - { - "id": 5338, - "slug": "generate-ai-viral-videos-with-seedance-and-upload-to-tiktok--5338", - "name": "Generate AI Viral Videos with Seedance and Upload to TikTok, YouTube & Instagram", - "description": "![Workflow Screenshot](https://www.dr-firas.com/seedanceII.png)\n\n# Generate AI videos with Seedance & Blotato, upload to TikTok, YouTube & Instagram\n\n### Who is this for?\n\nThis template is ideal for creators, content marketers, social media managers, and AI enthusiasts who want to automate the production of short-form, visually captivating videos for platforms like TikTok, YouTube Shorts, and Instagram Reels — all without manual editing or publishing.\n\n### What problem is this workflow solving?\n\nCreating engaging videos requires:\n- Generating creative ideas \n- Writing detailed scene prompts \n- Producing realistic video clips and sound effects \n- Editing and stitching the final video \n- Publishing across multiple platforms \n\nThis workflow automates the entire process, saving hours of manual work and ensuring consistent, AI-driven content output ready for social distribution.\n\n### What this workflow does\n\nThis end-to-end AI video automation workflow:\n\n1. **Generates a creative idea** using OpenAI and LangChain \n2. **Creates detailed video prompts** with Seedance AI \n3. **Generates video clips** via Wavespeed AI \n4. **Generates sound effects** with Fal AI \n5. **Stitches the final video** using Fal AI’s ffmpeg API \n6. **Logs metadata and video links** to Google Sheets \n7. **Uploads the video to Blotato** \n8. **Auto-publishes to TikTok, YouTube, Instagram, and other platforms**\n\n### Setup\n\n1. Add your **OpenAI API key** in the LLM nodes \n2. Set up **Seedance and Wavespeed AI credentials** for video prompt and clip generation \n3. Add your **Fal AI API key** for sound and stitching steps \n4. Connect your **Google Sheets account** for tracking ideas and outputs \n5. Set your **Blotato API key** and fill in the platform account IDs in the `Assign Social Media IDs` node \n6. Adjust the **Schedule Trigger** to control when the automation runs \n\n### How to customize this workflow to your needs\n\n- **Change the AI prompts** to target your niche (e.g., ASMR, product videos, humor) \n- **Add a Telegram or Slack step** for video preview before publishing \n- **Tweak scene structure** or video duration to match your style \n- **Disable platforms** you don’t want by turning off specific HTTP Request nodes \n- **Edit the sound generation prompts** for different moods or effects \n\n📄 **Documentation**: [Notion Guide](https://automatisation.notion.site/Generate-AI-Videos-with-Seedance-Blotato-and-Upload-to-TikTok-YouTube-Instagram-version-II-21d3d6550fd980218096d84f31bfae2d?source=copy_link)\n\n---\n\n### Need help customizing?\nContact me for consulting and support : [Linkedin](https://www.linkedin.com/in/dr-firas/) / [Youtube](https:/https://www.youtube.com/@DRFIRASS)", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.scheduleTrigger", - "@n8n/n8n-nodes-langchain.toolThink", - "@n8n/n8n-nodes-langchain.outputParserStructured", - "n8n-nodes-base.googleSheets", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.code", - "n8n-nodes-base.wait", - "n8n-nodes-base.httpRequest", - "@n8n/n8n-nodes-langchain.agent", - "@blotato/n8n-nodes-blotato.blotato", - "n8n-nodes-base.merge" - ], - "tags": ["trigger:schedule", "ai", "integration:blotato"], - "triggerType": "schedule", - "hasAI": true, - "score": 89.35, - "scoreBreakdown": { - "traction": 0.887, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.324 - }, - "source": "https://n8n.io/workflows/5338", - "author": "drfiras", - "success": true - }, - { - "id": 4827, - "slug": "ai-powered-whatsapp-chatbot-for-text-voice-images-and-pdf-wi-4827", - "name": "AI-Powered WhatsApp Chatbot for Text, Voice, Images, and PDF with RAG", - "description": "### Who is this for?\nThis template is designed for internal support teams, product specialists, and knowledge managers in technology companies who want to automate ingestion of product documentation and enable AI-driven, retrieval-augmented question answering via WhatsApp.\n\n### What problem is this workflow solving?\nSupport agents often spend too much time manually searching through lengthy documentation, leading to inconsistent or delayed answers. This solution automates importing, chunking, and indexing product manuals, then uses retrieval-augmented generation (RAG) to answer user queries accurately and quickly with AI via WhatsApp messaging.\n\n### What these workflows do\n\n#### Workflow 1: Document Ingestion & Indexing\n\n- Manually triggered to import product documentation from Google Docs.\n- Automatically splits large documents into chunks for efficient searching.\n- Generates vector embeddings for each chunk using OpenAI embeddings.\n- Inserts the embedded chunks and metadata into a MongoDB Atlas vector store, enabling fast semantic search.\n\n#### Workflow 2: AI-Powered Query & Response via WhatsApp\n\n- Listens for incoming WhatsApp user messages, supporting various types:\n\t- Text messages: Plain text queries from users.\n\t- Audio messages: Voice notes transcribed into text for processing.\n\t- Image messages: Photos or screenshots analyzed to provide contextual answers.\n\t- Document messages: PDFs, spreadsheets, or other files parsed for relevant content.\n- Converts incoming queries to vector embeddings and performs similarity search on the MongoDB vector store.\n- Uses OpenAI’s GPT-4o-mini model with retrieval-augmented generation to produce concise, context-aware answers.\n- Maintains conversation context across multiple turns using a memory buffer node.\n- Routes different message types to appropriate processing nodes to maximize answer quality.\n\n### Setup\n\n#### Setting up vector embeddings\n\n1. Authenticate Google Docs and connect your Google Docs URL containing the product documentation you want to index.\n2. Authenticate MongoDB Atlas and connect the collection where you want to store the vector embeddings. Create a search index on this collection to support vector similarity queries.\n3. Ensure the index name matches the one configured in n8n (data_index).\n4. See the example MongoDB search index template below for reference.\n\n#### Setting up chat\n\n1. Authenticate the WhatsApp node with your Meta account credentials to enable message receiving and sending.\n2. Connect the MongoDB collection containing embedded product documentation to the MongoDB Vector Search node used for similarity queries.\n3. Set up the system prompt in the Knowledge Base Agent node to reflect your company’s tone, answering style, and any business rules, ensuring it references the connected MongoDB collection for context retrieval.\n\n### Make sure\n\nBoth MongoDB nodes (in ingestion and chat workflows) are connected to the same collection with:\n\nAn embedding field storing vector data,\n\nRelevant metadata fields (e.g., document ID, source), and\n\nThe same vector index name configured (e.g., data_index).\n\n### Search Index Example:\n\n{\n \"mappings\": {\n \"dynamic\": false,\n \"fields\": {\n \"_id\": { \"type\": \"string\" },\n \"text\": { \"type\": \"string\" },\n \"embedding\": {\n \"type\": \"knnVector\",\n \"dimensions\": 1536,\n \"similarity\": \"cosine\"\n },\n \"source\": { \"type\": \"string\" },\n \"doc_id\": { \"type\": \"string\" }\n }\n }\n}", - "nodes": [ - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.embeddingsOpenAi", - "n8n-nodes-base.manualTrigger", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "@n8n/n8n-nodes-langchain.vectorStoreMongoDBAtlas", - "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", - "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter", - "n8n-nodes-base.googleDocs", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.whatsAppTrigger", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.extractFromFile", - "n8n-nodes-base.set", - "n8n-nodes-base.code", - "n8n-nodes-base.whatsApp", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.switch" - ], - "tags": ["trigger:manual", "ai", "integration:langchain"], - "triggerType": "manual", - "hasAI": true, - "score": 89.26, - "scoreBreakdown": { - "traction": 0.83, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.531 - }, - "source": "https://n8n.io/workflows/4827", - "author": "thomasgpt", - "success": true - }, - { - "id": 5110, - "slug": "create-upload-ai-generated-asmr-youtube-shorts-with-seedance-5110", - "name": "Create & Upload AI-Generated ASMR YouTube Shorts with Seedance, Fal AI, and GPT-4", - "description": "![Workflow_Screenshot.png](fileId:1559)\n\n//ASMR AI Workflow \n### Who is this for?\n\nContent Creators, YouTube Automation Enthusiasts, and AI Hobbyists looking to autonomously generate and publish unique, satisfying ASMR-style YouTube Shorts without manual effort.\n\n### What problem does this solve?\n\nThis workflow solves the creative bottleneck and time-consuming nature of daily content creation. It fully automates the entire production pipeline, from brainstorming trendy ideas to publishing a finished video, turning your n8n instance into a 24/7 content factory.\n\n### What this workflow does\n\n**1. Two-Stage AI Ideation & Planning:**\n* Uses an initial AI agent to brainstorm a short, viral ASMR concept based on current trends.\n* A second \"Planning\" AI agent then takes this concept and expands it into a detailed, structured production plan, complete with a viral-optimized caption, hashtags, and descriptions for the environment and sound.\n\n**2. Multi-Modal Asset Generation:**\n* **Video:** Feeds detailed scene prompts to the **ByteDance Seedance** text-to-video model (via Wavespeed AI) to generate high-quality video clips.\n* **Audio:** Simultaneously calls the **Fal AI** text-to-audio model to create custom, soothing ASMR sound effects that match the video's theme.\n* **Assembly:** Automatically sequences the video clips and sound into a single, cohesive final video file using an FFMPEG API call.\n\n**3. Closed-Loop Publishing & Logging:**\n* **Logging:** Initially logs the new idea to a Google Sheet with a status of \"In Progress\".\n* **Publishing:** Automatically uploads the final, assembled video directly to your YouTube channel, setting the title and description from the AI's plan.\n* **Updating:** Finds the original row in the Google Sheet and updates its status to \"Done\", adding a direct link to the newly published YouTube video.\n* **Notifications:** Sends real-time alerts to Telegram and/or Gmail with the video title and link, confirming the successful publication.\n\n### Setup\n\n**Credentials:**\nYou will need to create credentials in your n8n instance for the following services:\n* OpenAI API\n* Wavespeed AI API (for Seedance)\n* Fal AI API\n* Google OAuth Credential (enable **YouTube Data API v3** and **Google Sheets API** in your Google Cloud Project)\n* Telegram Bot Credential\n* (Optional) Gmail OAuth Credential\n\n**Configuration:**\nThis is an advanced workflow. The initial setup should take approximately 15-20 minutes.\n* **Google Sheet:** Create a Google Sheet with these columns: `idea`, `caption`, `production_status`, `youtube_url`. Add the **Sheet ID** to the Google Sheets nodes in the workflow.\n* **Node Configuration:** In the `Telegram Notification` node, enter your own `Chat ID`. In the `Gmail Notification` node, update the recipient email address.\n* **Activate:** Once configured, save and set the workflow to \"Active\" to let it run on its schedule.\n\n### How to customize\n\n* **Creative Direction:** To change the style or theme of the videos (e.g., from kinetic sand to soap cutting), simply edit the `systemMessage` in the **\"2. Enrich Idea into Plan\"** and **\"Prompts AI Agent\"** nodes.\n\n* **Initial Ideas:** To influence the AI's starting concepts, modify the prompt in the **\"1. Generate Trendy Idea\"** node.\n* **Video & Sound:** To change the video duration or sound style, adjust the parameters in the **\"Create Clips\"** and **\"Create Sounds\"** nodes.\n* **Notifications:** Add or remove notification channels (like Slack or Discord) after the **\"Upload to YouTube\"** node.\n", - "nodes": [ - "n8n-nodes-base.code", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.wait", - "@n8n/n8n-nodes-langchain.toolThink", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.outputParserStructured", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.youTube", - "n8n-nodes-base.telegram", - "n8n-nodes-base.gmail", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.googleSheets" - ], - "tags": ["trigger:schedule", "ai", "integration:langchain"], - "triggerType": "schedule", - "hasAI": true, - "score": 88.91, - "scoreBreakdown": { - "traction": 0.834, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.444 - }, - "source": "https://n8n.io/workflows/5110", - "author": "bilsimaging", - "success": true - }, - { - "id": 5385, - "slug": "lead-generation-system-google-maps-to-email-scraper-with-goo-5385", - "name": "Lead Generation System: Google Maps to Email Scraper with Google Sheets Export", - "description": "# Google Maps Email Scraper System\n\n**Categories:** Lead Generation, Web Scraping, Business Automation\n\nThis workflow creates a completely free Google Maps email scraping system that extracts unlimited business emails without requiring expensive third-party APIs. Built entirely in N8N using simple HTTP requests and JavaScript, this system can generate thousands of targeted leads for any industry or location while operating at 99% free cost structure.\n\n## **Benefits**\n\n- **Zero API Costs** - Operates entirely through free Google Maps scraping without expensive third-party services\n- **Unlimited Lead Generation** - Extract emails from thousands of Google Maps listings across any industry\n- **Geographic Targeting** - Search by specific cities, regions, or business types for precise lead targeting\n- **Complete Automation** - From search query to organized email list with minimal manual intervention\n- **Built-in Data Cleaning** - Automatic duplicate removal, filtering, and data validation\n- **Scalable Processing** - Handle hundreds of businesses per search with intelligent rate limiting\n\n## **How It Works**\n\n**Google Maps Search Integration:**\n- Uses strategic HTTP requests to Google Maps search URLs\n- Processes search queries like \"Calgary + dentist\" to extract business listings\n- Bypasses API restrictions through direct HTML scraping techniques\n\n**Intelligent URL Extraction:**\n- Custom JavaScript regex patterns extract website URLs from Google Maps data\n- Filters out irrelevant domains (Google, schema, static files)\n- Returns clean list of actual business websites for processing\n\n**Smart Website Processing:**\n- Loop-based architecture prevents IP blocking through intelligent batching\n- Built-in delays and redirect handling for reliable scraping\n- Processes each website individually with error handling\n\n**Email Pattern Recognition:**\n- Advanced regex patterns identify email addresses within website HTML\n- Extracts contact emails, info emails, and administrative addresses\n- Handles multiple email formats and validation patterns\n\n**Data Aggregation & Cleaning:**\n- Automatically removes duplicate emails across all processed websites\n- Filters null entries and invalid email formats\n- Exports clean, organized email lists to Google Sheets\n\n## **Required Google Sheets Setup**\n\nCreate a Google Sheet with these exact column headers:\n\n**Search Tracking Sheet:**\n- `searches` - Contains your search queries (e.g., \"Calgary dentist\", \"Miami lawyers\")\n\n**Email Results Sheet:**\n- `emails` - Contains extracted email addresses from all processed websites\n\n**Setup Instructions:**\n1. Create Google Sheet with two tabs: \"searches\" and \"emails\"\n2. Add your target search queries to the searches tab (one per row)\n3. Connect Google Sheets OAuth credentials in n8n\n4. Update the Google Sheets document ID in all sheet nodes\n\nThe workflow reads search queries from the first sheet and exports results to the second sheet automatically.\n\n## **Business Use Cases**\n\n- **Local Service Providers** - Find competitors and potential partners in specific geographic areas\n- **B2B Sales Teams** - Generate targeted prospect lists for cold outreach campaigns \n- **Marketing Agencies** - Build industry-specific lead databases for client campaigns\n- **Real Estate Professionals** - Identify businesses in target neighborhoods for commercial opportunities\n- **Franchise Development** - Research potential markets and existing competition\n- **Market Research** - Analyze business density and contact information across regions\n\n## **Revenue Potential**\n\nThis system transforms lead generation economics:\n- $0 per lead vs. $2-5 per lead from paid databases\n- Process 1,000+ leads daily without hitting API limits\n- Sell as a service for $500-2,000 per industry/location\n- Perfect for agencies offering lead generation to local businesses\n\n**Difficulty Level:** Intermediate \n**Estimated Build Time:** 1-2 hours \n**Monthly Operating Cost:** $0 (completely free)\n\n## **Watch My Complete Build Process**\n\nWant to watch me build this entire system live from scratch? I walk through every single step - including the JavaScript code, regex patterns, error handling, and all the debugging that goes into creating a bulletproof scraping system.\n\n🎥 **Watch My Live Build:** \"[Scrape Unlimited Leads WITHOUT Paying for APIs (99% FREE)](https://www.youtube.com/watch?v=OroDNJl-pyc)\"\n\nThis comprehensive tutorial shows the real development process - including writing custom JavaScript, handling rate limits, and building systems that actually work at scale without getting blocked.\n\n## **Set Up Steps**\n\n**Basic Workflow Architecture:**\n- Set up manual trigger for testing and Google Sheets integration\n- Configure initial HTTP request node for Google Maps searches \n- Enable SSL ignore and response headers for reliable scraping\n\n**URL Extraction Code Setup:**\n- Configure JavaScript code node with custom regex patterns\n- Set up input data processing from Google Maps HTML responses\n- Implement URL filtering logic to remove irrelevant domains\n\n**Website Processing Pipeline:**\n- Add \"Split in Batches\" node for intelligent loop processing\n- Configure HTTP request nodes with proper delays and redirect handling\n- Set up error handling for websites that can't be scraped\n\n**Email Extraction System:**\n- Implement JavaScript code node with email-specific regex patterns\n- Configure email validation and format checking\n- Set up data aggregation for multiple emails per website\n\n**Data Cleaning & Export:**\n- Configure filtering nodes to remove null entries and duplicates\n- Set up \"Split Out\" node to aggregate emails into single list\n- Connect Google Sheets integration for organized data export\n\n**Testing & Optimization:**\n- Use limit nodes during testing to prevent IP blocking\n- Test with small batches before scaling to full searches\n- Implement proxy integration for high-volume usage\n\n## **Advanced Optimizations**\n\nScale the system with:\n- **Multi-Page Scraping:** Extract URLs from homepages, then scrape contact pages for more emails\n- **Proxy Integration:** Add residential proxies for unlimited scraping without rate limits\n- **Industry Templates:** Create pre-configured searches for different business types\n- **Contact Information Expansion:** Extract phone numbers, addresses, and social media profiles\n- **CRM Integration:** Automatically add leads to sales pipelines and marketing sequences\n\n## **Important Considerations**\n\n- **Rate Limiting:** Built-in delays prevent IP blocking during normal usage\n- **Scalability:** For high-volume usage, consider proxy services for unlimited requests\n- **Compliance:** Ensure proper usage rights for extracted contact information\n- **Data Quality:** System includes filtering but manual verification recommended for critical campaigns\n\n## **Check Out My Channel**\n\nFor more advanced automation systems and business-building strategies that generate real revenue, explore [my YouTube channel](https://www.youtube.com/@nicksaraev) where I share proven automation techniques used by successful agencies and entrepreneurs.", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.removeDuplicates", - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.wait", - "n8n-nodes-base.limit", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.code", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.filter", - "n8n-nodes-base.googleSheets" - ], - "tags": ["trigger:manual", "integration:removeDuplicates"], - "triggerType": "manual", - "hasAI": false, - "score": 88.89, - "scoreBreakdown": { - "traction": 0.778, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.667 - }, - "source": "https://n8n.io/workflows/5385", - "author": "nicksaraev", - "success": true - }, - { - "id": 5678, - "slug": "automate-email-filtering-ai-summarization-100-free-effective-5678", - "name": "Automate Email Filtering & AI Summarization. 100% free & effective, works 7/24 ", - "description": "![dddss.png](fileId:1796)\n## Good to know:\nThis workflow automatically processes incoming emails (you can filter them base on your needs) and creates concise AI-powered summaries, then logs them to a Google Sheets spreadsheet for easy tracking and analysis.\n\n## Who is this for?\n\n➖Business professionals who receive many emails and need quick summaries\n➖Customer service teams tracking email communications\n➖Project managers monitoring email correspondence\n➖Anyone who wants to automatically organize and summarize their email communications\n\n## What problem is this workflow solving?\n\nThis workflow solves the problem of email overload by automatically reading incoming emails, generating concise summaries using AI, and organizing them in a structured format. It eliminates the need to manually read through every email to understand the key points and maintains a searchable record of communications.\n\n## What this workflow does:\n\n✅Monitors your Gmail inbox for new emails\n✅Filters emails based on specific criteria (sender validation)\n✅Extracts key information (sender, date, subject, content)\n✅Uses AI to generate concise summaries of email content\n✅Automatically logs all data including the AI summary to a Google Sheets spreadsheet\n\n## How it works:\n\n1️⃣Gmail trigger monitors for new emails at specified intervals\n2️⃣Email data is processed and formatted using JavaScript\n3️⃣A conditional check validates the sender\n4️⃣AI agent (powered by Groq's language model) reads the email content and generates a summary\n5️⃣All information is automatically appended to a Google Sheets document\n\n## How to use:\n\nSet up Gmail OAuth2 credentials in n8n\nConfigure Google Sheets OAuth2 credentials\nSet up Groq API credentials for AI processing\nCreate a Google Sheets document and update the document ID\nCustomize the sender validation criteria as needed\nActivate the workflow\n\n## Requirements:\n\n✅n8n instance (cloud or self-hosted)\n✅Gmail account with OAuth2 access\n✅Google Sheets account\n✅AI API\n✅Basic understanding of n8n workflow\n\n## Customizing this workflow:\n\n🟢Modify the Gmail trigger filters to target specific labels or criteria\n🟢Adjust the sender validation logic in the conditional node\n🟢Customize the AI prompt to change summary style or focus\n🟢Add additional data fields to the Google Sheets output\n🟢Change the polling frequency for checking new emails\n🟢Switch to different AI models by replacing the Groq node\n", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.googleSheets", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.gmailTrigger", - "n8n-nodes-base.if", - "@n8n/n8n-nodes-langchain.lmChatGroq", - "n8n-nodes-base.code" - ], - "tags": ["trigger:gmail", "ai", "integration:langchain"], - "triggerType": "gmail", - "hasAI": true, - "score": 88.39, - "scoreBreakdown": { - "traction": 0.67, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/5678", - "author": "arre", - "success": true - }, - { - "id": 2860, - "slug": "ai-automated-hr-workflow-for-cv-analysis-and-candidate-evalu-2860", - "name": "AI Automated HR Workflow for CV Analysis and Candidate Evaluation", - "description": "### How it Works\n\nThis workflow automates the process of handling job applications by extracting relevant information from submitted CVs, analyzing the candidate's qualifications against a predefined profile, and storing the results in a Google Sheet. Here’s how it operates:\n\n1. **Data Collection and Extraction**: \n - The workflow begins with a form submission (`On form submission` node), which triggers the extraction of data from the uploaded CV file using the `Extract from File` node.\n - Two `informationExtractor` nodes (`Qualifications` and `Personal Data`) are used to parse specific details such as educational background, work history, skills, city, birthdate, and telephone number from the text content of the CV.\n\n2. **Processing and Evaluation**:\n - A `Merge` node combines the extracted personal and qualification data into a single output.\n - This merged data is then passed through a `Summarization Chain` that generates a concise summary of the candidate’s profile.\n - An `HR Expert` chain evaluates the candidate against a desired profile (`Profile Wanted`), assigning a score and providing considerations for hiring.\n - Finally, all collected and processed data including the evaluation results are appended to a Google Sheets document via the `Google Sheets` node for further review or reporting purposes [[9]].\n\n### Set Up Steps\n\nTo replicate this workflow within your own n8n environment, follow these steps:\n\n1. **Configuration**:\n - Begin by setting up an n8n instance if you haven't already; you can sign up directly on their website or self-host the application.\n - Import the provided JSON configuration into your n8n workspace. Ensure that all necessary credentials (e.g., Google Drive, Google Sheets, OpenAI API keys) are correctly configured under the Credentials section since some nodes require external service integrations like Google APIs and OpenAI for language processing tasks.\n\n2. **Customization**:\n - Adjust the parameters of each node according to your specific requirements. For example, modify the fields in the `formTrigger` node to match what kind of information you wish to collect from applicants.\n - Customize the prompts given to AI models in nodes like `Qualifications`, `Summarization Chain`, and `HR Expert` so they align with the type of analyses you want performed on the candidates' profiles.\n - Update the destination settings in the `Google Sheets` node to point towards your own spreadsheet where you would like the final outputs recorded.\n\n---\n\n\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/). ", - "nodes": [ - "n8n-nodes-base.formTrigger", - "n8n-nodes-base.extractFromFile", - "@n8n/n8n-nodes-langchain.informationExtractor", - "@n8n/n8n-nodes-langchain.chainSummarization", - "n8n-nodes-base.merge", - "n8n-nodes-base.set", - "n8n-nodes-base.googleSheets", - "@n8n/n8n-nodes-langchain.outputParserStructured", - "@n8n/n8n-nodes-langchain.chainLlm", - "n8n-nodes-base.googleDrive", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:formTrigger", "ai", "integration:langchain"], - "triggerType": "formTrigger", - "hasAI": true, - "score": 88.38, - "scoreBreakdown": { - "traction": 0.69, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.917 - }, - "source": "https://n8n.io/workflows/2860", - "author": "n3witalia", - "success": true - }, - { - "id": 5962, - "slug": "track-seo-keyword-rankings-with-bright-data-mcp-and-gpt-4o-a-5962", - "name": "Track SEO Keyword Rankings with Bright Data MCP and GPT-4o AI Analysis", - "description": "*This workflow contains community nodes that are only compatible with the self-hosted version of n8n.*\n\nThis workflow automatically monitors keyword rankings across search engines to track SEO performance and identify optimization opportunities. It saves you time by eliminating the need to manually check keyword positions and provides comprehensive ranking data for strategic SEO decision making.\n\n## Overview\n\nThis workflow automatically scrapes search engine results pages (SERPs) to track keyword rankings, competitor positions, and search features. It uses Bright Data to access search results without restrictions and AI to intelligently parse ranking data, track changes, and identify SEO opportunities.\n\n## Tools Used\n\n- **n8n**: The automation platform that orchestrates the workflow\n- **Bright Data**: For scraping search engine results without being blocked\n- **OpenAI**: AI agent for intelligent ranking analysis and SEO insights\n- **Google Sheets**: For storing keyword ranking data and tracking changes\n\n## How to Install\n\n1. **Import the Workflow**: Download the .json file and import it into your n8n instance\n2. **Configure Bright Data**: Add your Bright Data credentials to the MCP Client node\n3. **Set Up OpenAI**: Configure your OpenAI API credentials\n4. **Configure Google Sheets**: Connect your Google Sheets account and set up your ranking tracking spreadsheet\n5. **Customize**: Define target keywords and ranking monitoring parameters\n\n## Use Cases\n\n- **SEO Teams**: Track keyword performance and identify ranking improvements\n- **Content Marketing**: Monitor content ranking success and optimization needs\n- **Competitive Analysis**: Track competitor keyword rankings and strategies\n- **Digital Marketing**: Measure organic search performance and ROI\n\n## Connect with Me\n\n- **Website**: https://www.nofluff.online\n- **YouTube**: https://www.youtube.com/@YaronBeen/videos\n- **LinkedIn**: https://www.linkedin.com/in/yaronbeen/\n- **Get Bright Data**: https://get.brightdata.com/1tndi4600b25 (Using this link supports my free workflows with a small commission)\n\n#n8n #automation #keywordrankings #seo #searchrankings #brightdata #webscraping #seotools #n8nworkflow #workflow #nocode #ranktracking #keywordmonitoring #seoautomation #searchmarketing #organicseo #seoresearch #rankinganalysis #keywordanalysis #searchengines #seomonitoring #digitalmarketing #serp #keywordtracking #seoanalytics #searchoptimization #rankingreports #keywordresearch #seoinsights #searchperformance", - "nodes": [ - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-mcp.mcpClientTool", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.set", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.code", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.outputParserAutofixing", - "@n8n/n8n-nodes-langchain.outputParserStructured" - ], - "tags": ["trigger:schedule", "ai", "integration:langchain"], - "triggerType": "schedule", - "hasAI": true, - "score": 88.29, - "scoreBreakdown": { - "traction": 0.689, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.9 - }, - "source": "https://n8n.io/workflows/5962", - "author": "yaron-nofluff", - "success": true - }, - { - "id": 10000, - "slug": "auto-create-tiktok-videos-with-veed-io-ai-avatars-elevenlabs-10000", - "name": "Auto-Create TikTok Videos with VEED.io AI Avatars, ElevenLabs & GPT-4", - "description": "# 💥 Viral TikTok Video Machine: Auto-Create Videos with Your AI Avatar\n\n![Workflow Overview](https://www.dr-firas.com/workflow-veed.png)\n\n---\n\n### 🎯 Who is this for?\nThis workflow is for **content creators, marketers, and agencies** who want to use **Veed.io’s AI avatar technology** to produce short, engaging TikTok videos automatically. \nIt’s ideal for creators who want to appear on camera without recording themselves, and for teams managing multiple brands who need to generate videos at scale.\n\n---\n\n### ⚙️ What problem this workflow solves\nManually creating videos for TikTok can take hours — finding trends, writing scripts, recording, and editing. \nBy combining **Veed.io**, **ElevenLabs**, and **GPT-4**, this workflow transforms a simple Telegram input into a **ready-to-post TikTok video** featuring your **AI avatar powered by Veed.io** — speaking naturally with your cloned voice.\n\n---\n\n### 🚀 What this workflow does\nThis automation links Veed.io’s video-generation API with multiple AI tools:\n- Analyzes TikTok trends via **Perplexity AI** \n- Writes a 10-second **viral script** using **GPT-4** \n- Generates your **voiceover** via **ElevenLabs** \n- Uses **Veed.io (Fabric 1.0 via FAL.ai)** to animate your avatar and sync the lips to the voice \n- Creates an engaging **caption + hashtags** for TikTok virality \n- Publishes the video automatically via **Blotato TikTok API** \n- Logs all results to **Google Sheets** for tracking\n\n---\n\n### 🧩 Setup\n1. **Telegram Bot**\n - Create your bot via [@BotFather](https://t.me/BotFather)\n - Configure it as the trigger for sending your **photo** and **theme**\n\n2. **Connect Veed.io**\n - Create an account on [Veed.io](https://www.veed.io) \n - Get your **FAL.ai API key** (Veed Fabric 1.0 model) \n - Use HTTPS image/audio URLs compatible with Veed Fabric \n\n3. **Other APIs**\n - Add **Perplexity**, **ElevenLabs**, and **Blotato TikTok** keys \n - Connect your Google Sheet for logging results \n\n---\n\n### 🛠️ How to customize this workflow\n- **Change your Avatar:** Upload a new image through Telegram, and **Veed.io** will generate a new talking version automatically. \n- **Modify the Script Style:** Adjust the GPT prompt for tone (educational, funny, storytelling). \n- **Adjust Voice Tone:** Tweak **ElevenLabs** stability and similarity settings. \n- **Expand Platforms:** Add Instagram, YouTube Shorts, or X (Twitter) posting nodes. \n- **Track Performance:** Customize your Google Sheet to measure your most successful Veed.io-based videos.\n\n---\n\n### 🧠 Expected Outcome\nIn just a few seconds after sending your photo and theme, this workflow — powered by **Veed.io** — creates a **fully automated TikTok video** featuring your AI avatar with natural lip-sync and voice. \nThe result is a continuous stream of **viral short videos**, made without cameras, editing, or effort.\n\n---\n\n✅ **Import the JSON file in n8n**, add your API keys (including Veed.io via FAL.ai), and start generating viral TikTok videos starring your AI avatar today!\n\n### 🎥 [Watch This Tutorial](https://youtu.be/EOe7h1resHk)\n\n![VEED logo](https://www.dr-firas.com/VEED.png)\n\n---\n📄 **Documentation**: [Notion Guide](https://automatisation.notion.site/Viral-TikTok-Video-Machine-Auto-Create-Videos-with-Your-AI-Avatar-2943d6550fd9804a947ae762d60d76f5?source=copy_link)\n\n### Need help customizing?\nContact me for consulting and support : [Linkedin](https://www.linkedin.com/in/dr-firas/) / [Youtube](https://www.youtube.com/channel/UCriIQI8uaoEro5FEnOpeidQ)", - "nodes": [ - "n8n-nodes-base.telegramTrigger", - "n8n-nodes-base.set", - "n8n-nodes-base.perplexity", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.telegram", - "n8n-nodes-base.code", - "n8n-nodes-base.wait", - "@blotato/n8n-nodes-blotato.blotato", - "n8n-nodes-base.merge" - ], - "tags": ["trigger:telegram", "ai", "integration:blotato"], - "triggerType": "telegram", - "hasAI": true, - "score": 87.92, - "scoreBreakdown": { - "traction": 0.798, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.393 - }, - "source": "https://n8n.io/workflows/10000", - "author": "drfiras", - "success": true - }, - { - "id": 3586, - "slug": "ai-powered-whatsapp-chatbot-for-text-voice-images-pdfs-with--3586", - "name": "AI-Powered WhatsApp Chatbot 🤖📲 for Text, Voice, Images & PDFs with memory 🧠", - "description": "This workflow is a highly advanced **multimodal AI assistant** designed to operate through **WhatsApp**. It can **understand and respond** to text, images, voice messages, and PDF documents by combining **OpenAI models** with smart logic to adapt to the content received.\n\n---\n\n### 🎯 **Core Features**\n\n#### 📥 1. **Automatic Message Type Detection**\nUsing the `Input type` node, the bot detects whether the user has sent:\n- Text\n- Voice messages\n- Images\n- Files (PDF)\n- Other unsupported content\n\n#### 💬 2. **Smart Text Message Handling**\n- Text messages are processed by an **OpenAI GPT-4o-mini agent** with a customized system prompt.\n- Replies are concise, accurate, and formatted for mobile readability.\n\n#### 🖼️ 3. **Image Analysis & Description**\n- Images are downloaded, converted to base64, and analyzed by an **image-aware AI model**.\n- The output is a rich, structured description, designed for visually impaired users or visual content interpretation.\n\n#### 🎙️ 4. **Voice Message Transcription & Reply**\n- Audio messages are downloaded and transcribed using OpenAI Whisper.\n- The transcribed text is analyzed and answered by the AI.\n- Optionally, the AI reply can be **converted back to voice** using **OpenAI's text-to-speech**, and sent as an audio message.\n\n#### 📄 5. **PDF Document Extraction & Summary**\n- Only PDFs are allowed (filtered via MIME type).\n- The document’s content is extracted and combined with the user's message.\n- The AI then provides a relevant summary or answer.\n\n#### 🧠 6. **Contextual Memory**\n- Each user has a personalized session ID with a memory window of 10 interactions.\n- This ensures a more natural and contextual conversation flow.\n\n\n---\n\n### **How It Works** \n\nThisworkflow is designed to handle incoming WhatsApp messages and process different types of inputs (**text, audio, images, and PDF documents**) using **AI-powered analysis**. Here’s how it functions: \n\n- **Trigger**: The workflow starts with the **WhatsApp Trigger** node, which listens for incoming messages (text, audio, images, or documents). \n- **Input Routing**: The **Input type** (Switch node) checks the message type and routes it to the appropriate processing branch: \n - **Text**: Directly forwards the message to the AI agent for response generation. \n - **Audio**: Downloads the audio file, transcribes it using OpenAI, and sends the transcription to the AI agent. \n - **Image**: Downloads the image, analyzes it with OpenAI’s GPT-4 model, and generates a detailed description. \n - **PDF Document**: Downloads the file, extracts text, and processes it with the AI agent. \n - **Unsupported Formats**: Sends an error message if the input is not supported. \n- **AI Processing**: The **AI Agent1** node, powered by OpenAI, processes the input (text, transcribed audio, image description, or PDF content) and generates a response. \n- **Response Handling**: \n - For **audio inputs**, the AI’s response is converted back into speech (using OpenAI’s TTS) and sent as a voice message. \n - For **other inputs**, the response is sent as a text message via WhatsApp. \n- **Memory**: The **Simple Memory** node maintains conversation context for follow-up interactions. \n\n### **Setup Steps** \nTo deploy this workflow in n8n, follow these steps: \n\n1. **Configure WhatsApp API Credentials**: \n - Set up **WhatsApp Business API** credentials (Meta Developer Account). \n - Add the credentials in the **WhatsApp Trigger**, **Get Image/Audio/File URL**, and **Send Message** nodes. \n\n**Set Up OpenAI Integration**: \n - Provide an **OpenAI API key** in the **Analyze Image**, **Transcribe Audio**, **Generate Audio Response**, and **AI Agent1** nodes. \n\n **Adjust Input Handling (Optional)**: \n - Modify the **Switch node (\"Input type\")** to handle additional message types if needed. \n - Update the **\"Only PDF File\" IF node** to support other document formats. \n\n**Test & Deploy**: \n - Activate the workflow and test with different message types (text, audio, image, PDF). \n - Ensure responses are correctly generated and sent back via WhatsApp. \n\n---\n\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/). ", - "nodes": [ - "n8n-nodes-base.whatsAppTrigger", - "n8n-nodes-base.httpRequest", - "@n8n/n8n-nodes-langchain.openAi", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.extractFromFile", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "n8n-nodes-base.whatsApp", - "n8n-nodes-base.if", - "n8n-nodes-base.code", - "n8n-nodes-base.set", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.switch" - ], - "tags": ["trigger:other", "ai", "integration:whatsApp"], - "triggerType": "other", - "hasAI": true, - "score": 87.87, - "scoreBreakdown": { - "traction": 0.778, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.462 - }, - "source": "https://n8n.io/workflows/3586", - "author": "n3witalia", - "success": true - }, - { - "id": 13270, - "slug": "use-skills-in-n8n-agent-node-13270", - "name": "Use skills In n8n agent node", - "description": "This template gives you a framework to use skills in any n8n agent. You can use this as a starting point and add any other tools or patterns needed for your use case.\n\n### What are “skills”?\n\nSkills are a context management standard created by Anthropic for use in Claude code. Basically, instead of having a HUGE system prompt, skills split that into lots of small, structured instruction files that tell an agent *how* to do a specific kind of task. Instead of stuffing a massive prompt full of rules, the agent:\n\n* finds the relevant skill file\n* reads it\n* and follows the steps inside\n\nIt’s a simple pattern that makes managing system prompts for general purpose agents much more straightforward.\n\nSee an example of a skills repo [here](https://github.com/anthropics/knowledge-work-plugins).\n\n### What this workflow does\n\n* Responds to messages in n8n Chat (or Chat Hub)\n* Builds an “available skills” index from one or more GitHub repos\n* Lets the agent browse folders + fetch skill files (Markdown) as needed\n* Uses the skill content to guide how it completes tasks\n\n### How it (roughly) works\n\n1. A chat message comes in.\n2. The workflow lists directories in the configured skills repos (root + `/skills` if present), filters out noise, and merges everything into one directory map.\n3. That directory map gets injected into the agent’s system prompt so it knows what skill files exist.\n4. When it needs instructions, it uses tools:\n\n * **List Files by Path Name** to explore folders\n * **Get a File From GitHub** to pull the skill file as raw text (no base64)\n\n### Same “skills” pattern as the CLI tools\n\nThe flow is: find a skill → open it → follow the steps, the same way it works in the CLI tools, but running inside n8n, so you **don’t need to download or install anything locally**.\n\n### How to set it up\n1. (Required) Add your GitHub credentials to each node that needs it\n2. (Required) Add your OpenRouter credentials to the chat node or replace with the provider of your choosing\n3. (Optional) Add more repos to `SKILLS_REPOS` (any skills GitHub repo works as long as your credentials have access to it, such as any public repo)\n4. (Optional) Add more tools and turn it into whatever agent you actually need", - "nodes": [ - "@n8n/n8n-nodes-langchain.chatTrigger", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.github", - "n8n-nodes-base.set", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "@n8n/n8n-nodes-langchain.lmChatOpenRouter", - "n8n-nodes-base.filter", - "n8n-nodes-base.githubTool", - "n8n-nodes-base.httpRequestTool", - "n8n-nodes-base.merge" - ], - "tags": ["trigger:chatTrigger", "ai", "integration:langchain"], - "triggerType": "chatTrigger", - "hasAI": true, - "score": 87.62, - "scoreBreakdown": { - "traction": 0.669, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.846 - }, - "source": "https://n8n.io/workflows/13270", - "author": "liammcgarrigle", - "success": true - }, - { - "id": 4484, - "slug": "build-a-voice-ai-chatbot-with-elevenlabs-and-infranodus-know-4484", - "name": "Build a Voice AI Chatbot with ElevenLabs and InfraNodus Knowledge Experts", - "description": "## Set Up ElevenLabs Voice Chat Agent using Graph RAG Knowledge Graphs as Experts\n\nThis workflow creates an AI voice chatbot agent that has access to several knowledge bases at the same time (used as \"experts\"). \n\nThese knowledge bases are provided using the [InfraNodus GraphRAG](https://infranodus.com/use-case/ai-knowledge-graphs) using the knowledge graphs and providing high-quality responses without the need to set up complex RAG vector store workflows.\n\nWe use [ElevenLabs](https://elevenlabs.io) to set up a voice agent that can be embedded to any website or used via their API.\n\nThe advantages of using GraphRAG instead of the standard vector stores for knowledge are:\n\n- Easy and quick to set up (no complex data import workflows needed) and to update with new knowledge\n- A knowledge graph has a holistic overview of your knowledge base\n- Better retrieval of relations between the document chunks = higher quality responses\n- Ability to reuse in other n8n workflows\n\n![InfraNodus knowledge graph](https://support.noduslabs.com/hc/article_attachments/20319018287260)\n\n\n## How it works\nThis template uses the n8n AI agent node as an orchestrating agent that decides which tool (knowledge graph) to use based on the user's prompt. \n\nThe user's prompt is received from the ElevenLabs Conversational AI agent via an n8n Webhook, which also takes care of the voice interaction. \n\nThe response from n8n is then sent to the Webhook, which is polled by the ElevenLabs voice agent. This agent processes the response and provides the final answer.\n\nHere's a description step by step:\n\n- The user submits a question using ElevenLabs voice interface\n- The question is sent via the `knowledge_base` tool in ElevenLabs to the n8n Webhook with the POST request containing the user's `prompt` and `sessionID` for Chat Memory node in n8n.\n- The n8n AI agent node checks a list of tools it has access to. Each tool has a description of the knowledge auto-generated by InfraNodus (we call each tool an \"expert\"). \n- The n8n AI agent decides which tool should be used to generate a response. It may reformulate user's query to be more suitable for the expert. \n- The query is then sent to the InfraNodus HTTP node endpoint, which will query the graph that corresponds to that expert. \n- Each InfraNodus GraphRAG expert provides a rich response that takes the whole context into account and provides a response from each expert (graph) along with a list of relevant statements retrieved using a combination or RAG and GraphRAG. \n- The n8n AI Agent node integrates the responses received from the experts to produce the final answer.\n- The final answer is sent back to the Webhook endpoint\n- ElevenLabs conversational AI agent picks up the response arriving from the `knowledge_base` tool via the webhook and then condenses it for conversational format and transforms text into voice.\n\n## How to use\n\nYou need an [InfraNodus GraphRAG API account and key](https://infranodus.com/use-case/ai-knowledge-graphs) to use this workflow. \n\n- Create an InfraNodus account\n- Get the API key at [https://infranodus.com/api-access](https://infranodus.com/api-access) and create a Bearer authorization key for the InfraNodus HTTP nodes.\n- Create a separate knowledge graph for each expert (using PDF / content import options) in InfraNodus\n- For each graph, go to the workflow, paste the name of the graph into the `body` `name` field.\n- Keep other settings intact or learn more about them at the [InfraNodus access points](https://support.noduslabs.com/hc/en-us/articles/13605983537692-InfraNodus-API-Access-Points) page. \n- Once you add one or more graphs as experts to your flow, add the LLM key to the OpenAI node and launch the workflow\n- You will also need to set up an ElevenLabs account and to set up a conversational AI agent there. See the Post note in the n8n workflow for a complete step-by-step description or our [support article on setting up ElevenLabs AI voice agent](https://support.noduslabs.com/hc/en-us/articles/20318967066396-How-to-Build-a-Text-Voice-AI-Agent-Chatbot-with-n8n-Elevenlabs-and-InfraNodus)\n- Once the voice AI agent is ready, you might want to combine it with a [text AI chatbot workflow](https://n8n.io/workflows/4402-ai-chatbot-agent-with-a-panel-of-experts-using-infranodus-graphrag-knowledge/) so your users have a choice between the text and voice interaction. In that case, you may be interested to use our free open-source [website popup chat widget popupchat.dev](https://popupchat.dev) where you can create an embed code to add to your blog or website and allow the user to choose between the text and voice interaction.\n\n## Requirements\n\n- An [InfraNodus](https://infranodus.com/use-case/ai-knowledge-graphs) account and API key\n- An OpenAI (or any other LLM) API key\n- An ElevenLabs account\n\n## FAQ\n\n**1. How many \"experts\" should I aim for?**\n\nWe recommend to aim for the number of experts as the optimal number of people in a team, which is usually 2-7. If you add more experts, your AI orchestrating agent will have troubles choosing the most suitable \"expert\" tool for the user's query. You can mitigate this by specifying in the AI agent description that it can choose maximum 3-7 experts to provide a response.\n\n**2. Why use InfraNodus GraphRAG and not standard vector store for knowledge?**\n\nFirst, vector stores are complex to set up and to update. You'd need a separate workflow for that, decide on the vector dimensions, add metadata to your knowledge, etc.\nWith InfraNodus, you have a complete RAG / GraphRAG solution under the hood that is easy to set up and provides high-quality responses that takes the overall structure and the relations between your ideas into account. \n\n**3 Why not use ElevenLabs' own knowledge?**\n\nOne of the reasons is that you want your knowledge base to be in one place so you can reuse it in other n8n workflows. Another reason is that you will not have such a good separation between the \"experts\" when you converse with the agent. So the answers you get will be based on top matches from all the books / articles you upload, while with the InfraNodus GraphRAG setup you can better control which graphs are consulted as experts and have an explicit way to display this data.\n\n\n\n## Customizing this workflow\n\nYou can use this same workflow with a Telegram bot, so you can interact with it using Telegram. There are many more customizations available on our [GitHub repo for n8n workflows](https://github.com/infranodus/n8n-infranodus-workflow-templates).\n\nCheck out the **complete setup guide** for this workflow at [https://support.noduslabs.com/hc/en-us/articles/20318967066396-How-to-Build-a-Text-Voice-AI-Agent-Chatbot-with-n8n-Elevenlabs-and-InfraNodus](https://support.noduslabs.com/hc/en-us/articles/20318967066396-How-to-Build-a-Text-Voice-AI-Agent-Chatbot-with-n8n-Elevenlabs-and-InfraNodus)\n\nAlso check out the **video tutorial** with a demo:\n\n[![Video tutorial](https://img.youtube.com/vi/07-HZZQs5h0/sddefault.jpg)](https://www.youtube.com/watch?v=07-HZZQs5h0)\n\n\n\n\n", - "nodes": [ - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.httpRequestTool", - "n8n-nodes-base.webhook", - "n8n-nodes-base.respondToWebhook", - "@n8n/n8n-nodes-langchain.lmChatGoogleGemini" - ], - "tags": ["trigger:webhook", "ai", "integration:langchain"], - "triggerType": "webhook", - "hasAI": true, - "score": 87.53, - "scoreBreakdown": { - "traction": 0.701, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.7 - }, - "source": "https://n8n.io/workflows/4484", - "author": "infranodus", - "success": true - }, - { - "id": 2846, - "slug": "ai-voice-chatbot-with-elevenlabs-openai-for-customer-service-2846", - "name": "AI Voice Chatbot with ElevenLabs & OpenAI for Customer Service and Restaurants", - "description": "The \"Voice RAG Chatbot with ElevenLabs and OpenAI\" workflow in n8n is designed to create an interactive voice-based chatbot system that leverages both text and voice inputs for providing information. Ideal for shops, commercial activities and restaurants\n\n### How it works:\n\nHere's how it operates:\n\n1. **Webhook Activation**: The process begins when a user interacts with the voice agent set up on [ElevenLabs](https://try.elevenlabs.io/ahkbf00hocnu), triggering a webhook in n8n. This webhook sends a question from the user to the AI Agent node.\n2. **AI Agent Processing**: Upon receiving the query, the AI Agent node processes the input using predefined prompts and tools. It extracts relevant information from the knowledge base stored within the Qdrant vector database.\n3. **Knowledge Base Retrieval**: The Vector Store Tool node interfaces with the Qdrant Vector Store to retrieve pertinent documents or data segments matching the user’s query.\n4. **Text Generation**: Using the retrieved information, the OpenAI Chat Model generates a coherent response tailored to the user’s question.\n5. **Response Delivery**: The generated response is sent back through another webhook to ElevenLabs, where it is converted into speech and delivered audibly to the user.\n6. **Continuous Interaction**: For ongoing conversations, the Window Buffer Memory ensures context retention by maintaining a history of interactions, enhancing the conversational flow.\n\n### Set up steps:\n\nTo configure this workflow effectively, follow these detailed setup instructions:\n\n1. **ElevenLabs Agent Creation**:\n\t- Create a FREE account on [ElevenLabs](https://try.elevenlabs.io/ahkbf00hocnu)\n - Begin by creating an agent on ElevenLabs (e.g., named 'test_n8n').\n - Customize the first message and define the system prompt specific to your use case, such as portraying a character like a waiter at \"Pizzeria da Michele\".\n - Add a Webhook tool labeled 'test_chatbot_elevenlabs' configured to receive questions via POST requests.\n\n2. **Qdrant Collection Initialization**:\n - Utilize the HTTP Request nodes ('Create collection' and 'Refresh collection') to initialize and clear existing collections in Qdrant. Ensure you update placeholders `QDRANTURL` and `COLLECTION` accordingly.\n\n3. **Document Vectorization**:\n - Use Google Drive integration to fetch documents from a designated folder. These documents are then downloaded and processed for embedding.\n - Employ the Embeddings OpenAI node to generate embeddings for the downloaded files before storing them into Qdrant via the Qdrant Vector Store node.\n\n4. **AI Agent Configuration**:\n - Define the system prompt for the AI Agent node which guides its behavior and responses based on the nature of queries expected (e.g., product details, troubleshooting tips).\n - Link necessary models and tools including OpenAI language models and memory buffers to enhance interaction quality.\n\n5. **Testing Workflow**:\n - Execute test runs of the entire workflow by clicking 'Test workflow' in n8n alongside initiating tests on the ElevenLabs side to confirm all components interact seamlessly.\n - Monitor logs and outputs closely during testing phases to ensure accurate data flow between systems.\n\n6. **Integration with Website**:\n - Finally, integrate the chatbot widget onto your business website replacing placeholder AGENT_ID with the actual identifier created earlier on ElevenLabs.\n\nBy adhering to these comprehensive guidelines, users can successfully deploy a sophisticated voice-driven chatbot capable of delivering precise answers utilizing advanced retrieval-augmented generation techniques powered by OpenAI and ElevenLabs technologies.\n\n----\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/). ", - "nodes": [ - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.toolVectorStore", - "@n8n/n8n-nodes-langchain.vectorStoreQdrant", - "@n8n/n8n-nodes-langchain.embeddingsOpenAi", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.googleDrive", - "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", - "@n8n/n8n-nodes-langchain.textSplitterTokenSplitter", - "n8n-nodes-base.respondToWebhook", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.webhook", - "@n8n/n8n-nodes-langchain.memoryBufferWindow" - ], - "tags": ["trigger:manual", "ai", "integration:langchain"], - "triggerType": "manual", - "hasAI": true, - "score": 87.44, - "scoreBreakdown": { - "traction": 0.691, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.722 - }, - "source": "https://n8n.io/workflows/2846", - "author": "n3witalia", - "success": true - }, - { - "id": 10212, - "slug": "generate-funny-ai-videos-with-sora-2-and-auto-publish-to-tik-10212", - "name": "Generate Funny AI Videos with Sora 2 and Auto-Publish to TikTok", - "description": "This automation creates a fully integrated pipeline to **generate AI-powered videos**, **store them**, and **publish them on TikTok** — all automatically.\nIt connects **OpenAI Sora 2**, and **Postiz (for TikTok publishing)** to streamline content creation.\n\n---\n\n### **Key Benefits**\n\n✅ **Full Automation** – From text prompt to TikTok upload, everything happens automatically with no manual intervention once set up.\n✅ **Centralized Control** – Google Sheets acts as a simple dashboard to manage prompts, durations, and generated results.\n✅ **AI-Powered Creativity** – Uses **OpenAI Sora 2** for realistic video generation and **GPT-5** for optimized titles.\n✅ **Social Media Integration** – Seamlessly posts videos to **TikTok** via **Postiz**, ready for your audience.\n✅ **Scalable & Customizable** – Can easily be extended to other platforms like YouTube, Instagram, or LinkedIn.\n✅ **Time-Saving** – Eliminates repetitive steps like manual video uploads or caption writing.\n\n---\n\n### How it works\n\nThis workflow automates the end-to-end process of generating AI videos and publishing them to TikTok. It is triggered either manually or on a recurring schedule.\n\n1. **Trigger & Data Fetch:** The workflow starts by checking a specified Form for new entries. It looks for rows where a video has been requested (a \"PROMPT\" is filled) but not yet generated (the \"VIDEO\" column is empty).\n\n2. **AI Video Generation:** For each new prompt found, the workflow sends a request to the **Fal.ai Sora 2** model to generate a video. It then enters a polling loop, repeatedly checking the status of the generation request every 60 seconds until the video is \"COMPLETED\".\n\n3. **Post-Processing & Upload:** Once the video is ready, the workflow performs several actions in parallel:\n * **Fetch Video & Store:** It retrieves the final video URL, downloads the video file\n * **Generate Title:** It uses the **OpenAI GPT-4o-mini** model to analyze the original prompt and generate an optimized, engaging title for the video.\n * **Publish to TikTok:** The video file is uploaded to **Postiz**, a social media scheduling tool, which then automatically publishes it to a connected TikTok channel, using the AI-generated title as the post's caption.\n\n---\n### Set up steps\n\nTo make this workflow functional, you need to complete the following configuration steps:\n\n1. **Prepare the Google Sheet:**\n * Create a Form with at least \"PROMPT\", \"DURATION\", and \"VIDEO\" fields.\n\n2. **Configure Fal.ai for Video Generation:**\n * Create an account at [Fal.ai](https://fal.ai/) and obtain your API key.\n * In both the **\"Create Video\"** and **\"Get status\"** HTTP Request nodes, set up the \"Header Auth\" credential.\n * Set the `Name` to `Authorization` and the `Value` to `Key YOUR_API_KEY`.\n\n3. **Set up TikTok Publishing via Postiz:**\n * Create an account on [Postiz](https://postiz.com/) and connect your TikTok account to get a **Channel ID**.\n * Obtain your Postiz API key.\n * In the **\"Upload Video to Postiz\"** and **\"TikTok\" (Postiz)** nodes, configure the API credentials.\n * In the **\"TikTok\"** node, replace the placeholder `\"XXX\"` in the `integrationId` field with your actual TikTok Channel ID from Postiz.\n\n4. **(Optional) Configure AI Title Generation:**\n * The **\"Generate title\"** node uses OpenAI. Ensure you have valid OpenAI API credentials configured in n8n for this node to work.\n\n---\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/). \n## Header 2", - "nodes": [ - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.wait", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.if", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-postiz.postiz", - "n8n-nodes-base.formTrigger" - ], - "tags": ["trigger:formTrigger", "ai", "integration:httpRequest"], - "triggerType": "formTrigger", - "hasAI": true, - "score": 87.4, - "scoreBreakdown": { - "traction": 0.72, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.6 - }, - "source": "https://n8n.io/workflows/10212", - "author": "n3witalia", - "success": true - }, - { - "id": 4295, - "slug": "automate-business-lead-scraping-from-apify-to-google-sheets--4295", - "name": "Automate Business Lead Scraping from Apify to Google Sheets with Data Cleaning", - "description": "\n## 🚀 Automated Lead Scraper Workflow (Apify + n8n + Google Sheets)\n\n### 🧠 What It Does\n\nThis n8n workflow automates the process of scraping leads using **Apify**, cleaning the extracted data, and exporting it to **Google Sheets**—ready for use in outreach, prospecting, or CRM pipelines.\n\n---\n\n### 🔄 Workflow Steps\n\n1. ✅ **Start** – Manually triggers the workflow.\n2. 🧩 **Set Variables** – Stores required Apify credentials:\n\n * `APIFY_TOKEN`: Your Apify token.\n * `APIFY_TASK_ID`: The Apify task to run.\n3. 🕸️ **Run Apify Scraper** – Launches the scraper and fetches the dataset.\n4. 🧹 **Clean Data** – Processes scraped results to:\n\n * ✂️ Strip non-numeric characters from phone numbers.\n * ✉️ Format emails (lowercase + trimmed).\n5. 📊 **Export to Google Sheets** – Appends clean data to your spreadsheet:\n\n * 🏢 `company name` → from `title`\n * 📞 `phone` → cleaned number\n * 📍 `address` → from scraped info\n\n---\n\n### 🛠️ Requirements\n\n* 🕷️ **Apify Account**\n\n * A valid `APIFY_TOKEN`\n * An existing Apify task (`APIFY_TASK_ID`)\n\n* 📗 **Google Sheets Access**\n\n * OAuth2 credentials set up in n8n (e.g., `\"Google Sheets account 2\"`)\n\n---\n\n### 🚦 How to Use\n\n1. ⚙️ Open the **Variables** node and plug in your Apify credentials.\n2. 📄 Confirm the **Google Sheets** node points to your desired spreadsheet.\n3. ▶️ Run the workflow manually from the **Start** node.\n\n---\n\n### 📥 Output\n\nA ready-to-use sheet of **cleaned lead data** containing:\n\n* Company names\n* Phone numbers\n* Addresses\n\n---\n\n### 💼 Perfect For:\n\n* Sales teams doing outbound prospecting\n* Marketers building lead lists\n* Agencies running data aggregation tasks\n\n---\n\n", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.code", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.set", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:manual", "integration:httpRequest"], - "triggerType": "manual", - "hasAI": false, - "score": 87.22, - "scoreBreakdown": { - "traction": 0.611, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/4295", - "author": "dae221", - "success": true - }, - { - "id": 4005, - "slug": "ai-generated-linkedin-posts-with-openai-google-sheets-email--4005", - "name": "AI-Generated LinkedIn Posts with OpenAI, Google Sheets & Email Approval Workflow", - "description": "### How it works\nThis workflow automates the process of creating, approving, and optionally posting LinkedIn content from a Google Sheet. Here's a high-level overview:\n1. **Scheduled Trigger**: Runs automatically based on your defined time interval (daily, weekly, etc.).\n2. **Fetch Data from Google Sheets**: Pulls the first row from your sheet where Status is marked as Pending.\n3. **Generate LinkedIn Post Content**: Uses OpenAI to create a professional LinkedIn post using the Post Description and Instructions from the sheet.\n4. **Format & Prepare Data**: Formats the generated content along with the original instruction and post description for email.\n5. **Send for Approval**: Sends an email to a predefined user (e.g., marketing team) with a custom form for approval, including a dropdown to accept/reject and an optional field for edits.\n6. **(Optional) Image Fetch**: Downloads an image from a URL (if provided in the sheet) for future use in post visuals.\n\n\n### Set up steps\nYou’ll need the following before you start:\n\n- A Google Sheet with the following columns: Post Description, Instructions, Image (URL), Status\n- Access to an OpenAI API key\n- A connected Gmail account for sending approval emails\n- Your own Google Sheets and Gmail credentials added in n8n\n\n### Steps:\n1. Google Sheet Preparation:\nCreate a new Google Sheet with the mentioned columns (Post Description, Instructions, Image, Status, Output, Post Link).\nAdd a row with test data and set Status to Pending.\n\n2. Credentials:\nIn n8n, create OAuth2 credentials for:\na. Google Sheets\nb. Gmail\nc. OpenAI (API Key)\nAssign these credentials to the respective nodes in the JSON.\n\n3. OpenAI Model:\nChoose a model like gpt-4o-mini (used here) or any other available in your plan.\nAdjust the prompt in the \"Generate Post Content\" node if needed.\n\n4. Email Configuration:\nIn the Gmail node, set the recipient email to your own or your team’s address.\nCustomize the email message template if necessary.\n\n5. Schedule the Workflow:\nSet the trigger interval (e.g., every morning at 9 AM).\n\n6. Testing:\nRun the workflow manually first to confirm everything works.\nCheck Gmail for the approval form, respond, and verify the results.", - "nodes": [ - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.scheduleTrigger", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.googleSheets", - "@n8n/n8n-nodes-langchain.chainLlm", - "n8n-nodes-base.set", - "n8n-nodes-base.gmail", - "n8n-nodes-base.switch", - "n8n-nodes-base.if", - "n8n-nodes-base.linkedIn", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:schedule", "ai", "integration:langchain"], - "triggerType": "schedule", - "hasAI": true, - "score": 87.21, - "scoreBreakdown": { - "traction": 0.668, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.769 - }, - "source": "https://n8n.io/workflows/4005", - "author": "shindearyan", - "success": true - }, - { - "id": 5163, - "slug": "ai-powered-stock-analysis-assistant-with-telegram-claude-gpt-5163", - "name": "AI-Powered Stock Analysis Assistant with Telegram, Claude & GPT-4O Vision", - "description": "\"Ade Technical Analyst\" is a dual-workflow AI system combining conversational intelligence with visual chart analysis through Telegram. The system features 11 primary nodes for conversation management and 8 secondary nodes for chart generation and analysis.\n\nCore Components:\n\nTelegram Integration: Message handling with dynamic typing indicators\n\nAI Personality: \"Ade\" - a financial analyst with 50+ years NYSE/LSE experience using Claude 3.5 Sonnet\n\nChart Generation: TradingView integration via Chart-IMG API with MACD and volume indicators\n\nVisual Analysis: GPT-4O vision for technical pattern recognition\n\nMemory System: Session-based conversation context retention\n\nTarget Users\nIndividual traders seeking professional-grade analysis without subscription costs\n\nFinancial advisors wanting 24/7 AI-powered client support\n\nInvestment educators needing interactive learning tools\n\nFintech companies requiring white-label analysis solutions\n\nSetup Requirements\nCritical Security Fix Needed:\n\nRemove hardcoded API key from Chart-IMG node immediately\n\nStore all credentials securely in n8n credential manager\n\nRequired APIs:\n\nOpenRouter (Claude 3.5 Sonnet)\n\nOpenAI (GPT-4O vision)\n\nChart-IMG API\n\nTelegram Bot Token\n\nTechnical Prerequisites:\n\nn8n version 1.7+ with Langchain nodes\n\nWebhook configuration for Telegram\n\nDual-workflow setup with proper ID referencing\n\nWorkflow Requirements\nSecurity Compliance:\n\nNever hardcode API keys in workflow JSON files\n\nUse n8n credential manager for all sensitive data\n\nImplement proper session isolation for user data\n\nInclude mandatory financial disclaimers\n\nPerformance Specifications:\n\nModel temperature: 0.8 for balanced responses\n\nToken limit: 500 for optimized performance\n\nDark theme charts with professional indicators\n\nSession-based memory management\n\nNeed help customizing?\n[Contact](Contact) me for consulting and support or add me on [LinkedIn](LinkedIn)", - "nodes": [ - "@n8n/n8n-nodes-langchain.toolWorkflow", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.telegram", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.lmChatOpenRouter", - "n8n-nodes-base.set", - "n8n-nodes-base.merge", - "n8n-nodes-base.telegramTrigger", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "n8n-nodes-base.executeWorkflowTrigger", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.httpRequest" - ], - "tags": ["trigger:telegram", "ai", "integration:langchain"], - "triggerType": "telegram", - "hasAI": true, - "score": 87.19, - "scoreBreakdown": { - "traction": 0.698, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.647 - }, - "source": "https://n8n.io/workflows/5163", - "author": "hgray", - "success": true - }, - { - "id": 13526, - "slug": "generate-ai-videos-and-carousels-with-blotato-for-instagram--13526", - "name": "Generate AI videos and carousels with Blotato for Instagram and TikTok", - "description": "# Generate AI videos and carousels with Blotato and publish to Instagram & TikTok\n\n📄 **Documentation**: [Notion Guide](https://automatisation.notion.site/Turn-AI-Videos-Carousels-Into-Income-with-n8n-Fully-Automated-x-Blotato-30c3d6550fd9804b999ede955fdf409d?source=copy_link)\n\n![Workflow Overview](https://www.dr-firas.com/blotato-agent.png)\n\n### Who is this for?\n\nThis workflow is designed for **content creators, marketers, solopreneurs, and automation enthusiasts** who want to generate and publish short-form content on **Instagram and TikTok** automatically. \nIt is ideal for users looking to combine **AI-generated videos and carousels** with **Blotato** and orchestrate everything using **n8n**.\n\n---\n\n### What this workflow does\n\nThis workflow provides a complete **end-to-end automation pipeline**:\n\n1. Receives a message from **Telegram** containing a public URL and a publishing instruction.\n2. Creates a content source from the URL using **Blotato**.\n3. Retrieves and validates the extracted text content.\n4. Generates either:\n - An **AI tweet-card carousel** for Instagram, or\n - An **AI-generated video** for TikTok.\n5. Continuously checks the visual generation status until it is fully completed.\n6. Publishes the final media automatically to **Instagram or TikTok**.\n7. Sends a confirmation message back to Telegram once the post is successfully published.\n\n---\n\n### Setup\n\nTo use this workflow, you will need:\n\n- An active **n8n** instance\n- A **[Blotato](https://blotato.com/?ref=firas)** account with API access\n- Instagram and/or TikTok accounts connected in **[Blotato](https://blotato.com/?ref=firas)**\n- A **Telegram Bot** for triggering the workflow and receiving notifications\n\nSetup steps:\n1. Import the workflow JSON into n8n.\n2. Add your **[Blotato](https://blotato.com/?ref=firas)** API credentials.\n3. Configure the Telegram Trigger with your bot token.\n4. Select your Instagram and TikTok accounts in the **[Blotato](https://blotato.com/?ref=firas)** post nodes.\n5. Activate the workflow.\n\n---\n\n### How to customize this workflow to your needs\n\nYou can customize this workflow by:\n- Changing the visual templates used in Blotato.\n- Adjusting AI prompts to control tone, format, or content style.\n- Adding additional publishing platforms after the posting step.\n- Modifying polling behavior or adding timeouts for long visual renders.\n- Replacing Telegram with another trigger such as Webhooks or Slack.\n\nThe workflow is modular and easy to extend, making it suitable for a wide range of content automation use cases.\n\n\n### 🎥 [Watch This Tutorial](https://youtu.be/HaYFevp7KsU)\n\n![SORA2 logo](https://www.dr-firas.com/blotato-miniature.png)\n\n\n---\n### 👋 Need help or want to customize this?\n📩 Contact: [LinkedIn](https://www.linkedin.com/in/dr-firas/) \n📺 YouTube: [@DRFIRASS](https://www.youtube.com/@DRFIRASS) \n🚀 Workshops: [Mes Ateliers n8n](https://hotm.art/formation-n8n)\n\n\n### Need help customizing?\nContact me for consulting and support : [Linkedin](https://www.linkedin.com/in/dr-firas/) / [Youtube](https://www.youtube.com/channel/UCriIQI8uaoEro5FEnOpeidQ) / [🚀 Mes Ateliers n8n ](https://hotm.art/formation-n8n)", - "nodes": [ - "@blotato/n8n-nodes-blotato.blotatoTool", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.telegramTrigger", - "n8n-nodes-base.telegramTool", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:telegram", "ai", "integration:blotatoTool"], - "triggerType": "telegram", - "hasAI": true, - "score": 87.06, - "scoreBreakdown": { - "traction": 0.728, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.5 - }, - "source": "https://n8n.io/workflows/13526", - "author": "drfiras", - "success": true - }, - { - "id": 10640, - "slug": "scrape-google-maps-business-leads-with-apify-gpt-4-email-ext-10640", - "name": "Scrape Google Maps Business Leads with Apify & GPT-4 Email Extraction", - "description": "# 💥 Automate Scrape Google Maps Business Leads (Email, Phone, Website) using Apify\n\n![Workflow Overview](https://www.dr-firas.com/googlemaps.png)\n\n# 🧠 AI-Powered Business Prospecting Workflow (Google Maps + Email Enrichment)\n\n### Who is this for?\nThis workflow is designed for **entrepreneurs, sales teams, marketers, and agencies** who want to **automate lead discovery** and **build qualified business contact lists** — without manual searching or copying data. \nIt’s perfect for anyone seeking an **AI-driven prospecting assistant** that saves time, centralizes business data, and stays fully **compliant with GDPR**.\n\n---\n\n### What problem is this workflow solving?\nManually searching for potential clients, copying their details, and qualifying them takes hours — and often leads to messy spreadsheets. \nThis workflow **automates the process** by:\n- Gathering **publicly available business information** from Google Maps \n- Enriching that data with **AI-powered summaries and contact insights** \n- Compiling it into a clean, ready-to-use **Google Sheet database**\n\nThis means you can focus on **closing deals**, not collecting data.\n\n---\n\n### What this workflow does\nThis automation identifies, analyzes, and organizes business opportunities in just a few steps:\n1. **Telegram Trigger** → Send a message specifying your business type, number of leads, and Google Maps URL. \n2. **Apify Integration** → Fetches business information from Google Maps (public data). \n3. **Duplicate Removal** → Ensures clean, non-redundant results. \n4. **AI Summarization (GPT-4)** → Generates concise business summaries for better understanding. \n5. **Email Extraction (GPT-4)** → Finds and extracts professional contact emails from company websites. \n6. **Google Sheets Integration** → Automatically stores results (name, category, location, phone, email, etc.) in a structured sheet. \n7. **Telegram Notification** → Confirms when all businesses are processed. \n\nAll data is handled ethically and transparently — only from **public sources** and without any unsolicited contact.\n\n---\n\n### Setup\n1. **Telegram Setup** \n - Create a Telegram bot via [BotFather](https://t.me/BotFather) \n - Copy the API token and paste it into the Telegram Trigger node credentials. \n\n2. **Apify Setup** \n - Create an account on [Apify](https://www.apify.com?fpr=udemy) \n - Get your API token and connect it to the “Run Google Maps Scraper” node. \n\n3. **Google Sheets Setup** \n - Connect your Google account under the “Google Maps Database” node. \n - Specify the target spreadsheet and worksheet name. \n\n4. **OpenAI Setup** \n - Add your OpenAI API key to the AI nodes (“Company Summary Info” and “Extract Business Email”). \n\n5. **Test** \n - Send a Telegram message like: \n `restaurants, 5, https://www.google.com/maps/search/restaurants+in+Paris` \n\n---\n\n### How to customize this workflow to your needs\n- **Change search region or business type** by modifying the Telegram input message format. \n- **Adjust the number of leads** via the `maxCrawledPlacesPerSearch` parameter in Apify. \n- **Add filters or enrichments** (e.g., websites with social links, review counts, or opening hours). \n- **Customize AI summaries** by tweaking the prompt inside the “Company Summary Info” node. \n- **Integrate CRM tools** like HubSpot or Pipedrive by adding a connector after the Google Sheets node. \n\n---\n\n### ⚙️ Expected Outcome\n✅ A clean, enriched, and ready-to-use **Google Sheet** of businesses with: \n- Name, category, address, and city \n- Phone number and website \n- AI-generated business summary \n- Extracted professional email (if available) \n\n✅ Telegram confirmation once all businesses are processed \n\n✅ Fully automated, scalable, and GDPR-compliant prospecting workflow \n\n---\n\n💡 *This workflow provides a transparent, ethical way to streamline your B2B lead research while staying compliant with privacy and anti-spam regulations.*\n\n\n### 🎥 [Watch This Tutorial](https://youtu.be/cijBIHI6iLk)\n\n![Apify logo](https://www.dr-firas.com/apify.png)\n\n---\n### 👋 Need help or want to customize this?\n📩 Contact: [LinkedIn](https://www.linkedin.com/in/dr-firas/) \n📺 YouTube: [@DRFIRASS](https://www.youtube.com/@DRFIRASS) \n🚀 Workshops: [Mes Ateliers n8n](https://hotm.art/formation-n8n)\n\n---\n📄 **Documentation**: [Notion Guide](https://automatisation.notion.site/Automate-Scrape-Google-Maps-Business-Leads-Email-Phone-Website-using-Apify-2a53d6550fd98118ba22e441171944dd?source=copy_link)\n### Need help customizing?\nContact me for consulting and support : [Linkedin](https://www.linkedin.com/in/dr-firas/) / [Youtube](https://www.youtube.com/channel/UCriIQI8uaoEro5FEnOpeidQ) / [🚀 Mes Ateliers n8n ](https://hotm.art/formation-n8n)", - "nodes": [ - "@apify/n8n-nodes-apify.apify", - "n8n-nodes-base.removeDuplicates", - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.wait", - "n8n-nodes-base.telegramTrigger", - "n8n-nodes-base.code", - "n8n-nodes-base.telegram", - "n8n-nodes-base.set", - "n8n-nodes-base.httpRequest", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:telegram", "ai", "integration:apify"], - "triggerType": "telegram", - "hasAI": true, - "score": 86.91, - "scoreBreakdown": { - "traction": 0.649, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.786 - }, - "source": "https://n8n.io/workflows/10640", - "author": "drfiras", - "success": true - }, - { - "id": 4587, - "slug": "extract-instagram-profile-data-with-apify-and-store-in-googl-4587", - "name": "Extract Instagram Profile Data with Apify and Store in Google Sheets", - "description": "\n## Workflow Overview\nThis cutting-edge n8n automation is a powerful social media intelligence gathering tool designed to transform Instagram profile research into a seamless, automated process. By intelligently combining web scraping, data formatting, and cloud storage technologies, this workflow:\n\n1. **Discovers Profile Insights**: \n - Automatically scrapes Instagram profile data\n - Captures comprehensive profile metrics\n - Extracts critical social media intelligence\n\n2. **Intelligent Data Capture**:\n - Retrieves follower counts\n - Collects biographical information\n - Captures profile picture and external links\n\n3. **Seamless Data Logging**:\n - Automatically stores data in Google Sheets\n - Creates a living, updateable database\n - Enables easy analysis and tracking\n\n## Key Benefits\n- 🤖 **Full Automation**: Instant profile intelligence\n- 💡 **Comprehensive Insights**: Detailed social media metrics\n- 📊 **Effortless Tracking**: Automated data collection\n- 🌐 **Multi-Purpose Research**: Flexible data gathering\n\n## Workflow Architecture\n\n### 🔹 Stage 1: Trigger & Input\n- **Form-Based Trigger**: Manual username submission\n- **Webhook Support**: Flexible data entry methods\n- **User-Driven Initiation**\n\n### 🔹 Stage 2: Web Scraping\n- **Apify Integration**: Robust Instagram data extraction\n- **Comprehensive Profile Scanning**:\n - Followers count\n - Following count\n - Profile biography\n - Profile picture URL\n\n### 🔹 Stage 3: Data Formatting\n- **Intelligent Data Mapping**\n- **Standardized Data Structure**\n- **Preparation for Storage**\n\n### 🔹 Stage 4: Cloud Logging\n- **Google Sheets Integration**\n- **Persistent Data Storage**\n- **Easy Access and Analysis**\n\n## Potential Use Cases\n- **Influencer Marketing**: Talent identification\n- **Competitive Intelligence**: Audience research\n- **Social Media Analysis**: Performance tracking\n- **Recruitment**: Talent scouting\n- **Brand Partnerships**: Collaboration opportunities\n\n## Setup Requirements\n1. **Apify Account**\n - Instagram scraping actor\n - API token\n - Configured scraping parameters\n\n2. **Google Sheets**\n - Connected Google account\n - Prepared tracking spreadsheet\n - Appropriate sharing settings\n\n3. **n8n Installation**\n - Cloud or self-hosted instance\n - Workflow configuration\n - API credential management\n\n## Future Enhancement Suggestions\n- 🤖 Advanced profile scoring\n- 📊 Engagement rate calculation\n- 🔔 Real-time change alerts\n- 🌐 Multi-platform profile tracking\n- 🧠 AI-powered insights generation\n\n## Technical Considerations\n- Implement robust error handling\n- Use exponential backoff for API calls\n- Maintain flexible data extraction strategies\n- Ensure compliance with platform terms of service\n\n## Ethical Guidelines\n- Respect user privacy\n- Use data for legitimate research\n- Maintain transparent data collection practices\n- Provide opt-out mechanisms\n\n\n## Connect With Me\n\n**Ready to unlock social media insights?**\n\n📧 **Email**: Yaron@nofluff.online\n\n🎥 **YouTube**: [@YaronBeen](https://www.youtube.com/@YaronBeen/videos)\n\n💼 **LinkedIn**: [Yaron Been](https://www.linkedin.com/in/yaronbeen/)\n\n**Transform your social media research with intelligent, automated workflows!**\n\n#InstagramDataScraping #SocialMediaIntelligence #InfluencerMarketing #DataAutomation #AIResearch #MarketingTechnology #SocialMediaAnalytics #ProfileIntelligence #WebScraping #MarketingTech\n", - "nodes": [ - "n8n-nodes-base.formTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.set", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:formTrigger", "integration:httpRequest"], - "triggerType": "formTrigger", - "hasAI": false, - "score": 86.9, - "scoreBreakdown": { - "traction": 0.595, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/4587", - "author": "yaron-nofluff", - "success": true - }, - { - "id": 7456, - "slug": "automate-cv-screening-with-ai-candidate-analysis-7456", - "name": "🤖 Automate CV Screening with AI Candidate Analysis", - "description": "## How it works\n\nThis workflow automates your initial hiring pipeline by creating an AI-powered CV scanner. It collects job applications through a web form, uses AI to analyze the candidate's CV against your job description, and neatly organizes the results in a Google Sheet.\n\nHere’s the step-by-step process:\n* **The Application Form:** A `Form Trigger` provides a public web form for candidates to submit their name, email, and CV (as a PDF).\n* **Initial Logging:** As soon as an application is submitted, the candidate's name and email are added to a Google Sheet. This ensures every applicant is logged, even if a later step fails.\n* **CV Text Extraction:** The workflow uses **Mistral's OCR** model to accurately extract all the text from the uploaded CV PDF.\n* **AI Analysis:** The extracted text is sent to **Google Gemini**. A detailed prompt instructs the AI to act as a hiring assistant, scoring the CV against the specific requirements of your job role and providing a detailed explanation for its score.\n* **Structured Output:** A `JSON Output Parser` ensures the AI's analysis is returned in a clean, structured format, making the data reliable.\n* **Final Record:** The AI-generated qualification score and explanation are added to the candidate's row in the Google Sheet, giving you a complete, analyzed list of applicants.\n\n### Set up steps\n\n**Setup time: ~15 minutes**\n\nYou'll need API keys for Mistral and Google AI, and to connect your Google account.\n\n1. **Get Your Mistral API Key:**\n * Visit the Mistral Platform at [console.mistral.ai/api-keys](https://console.mistral.ai/api-keys).\n * Create and copy your API key.\n * In the workflow, go to the **`Extract CV Text`** node, click the **Credential** dropdown, and select **`+ Create New Credential`**.\n * Paste your key into the **API Key** field and **Save**.\n\n2. **Get Your Google AI API Key:**\n * Visit Google AI Studio at [aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey).\n * Click **\"Create API key in new project\"** and copy the key.\n * In the workflow, go to the **`Gemini 2.5 Flash Lite`** node, click the **Credential** dropdown, and select **`+ Create New Credential`**.\n * Paste your key into the **API Key** field and **Save**.\n\n3. **Connect Your Google Account:**\n * Select the **`Create 'CVs' Spreadsheet`** node.\n * Click the **Credential** dropdown and select **`+ Create New Credential`** to connect your Google account.\n * Repeat this for the **`Log Candidate Submission`** and **`Add CV Analysis`** nodes, selecting the credential you just created.\n\n4. **Create Your Spreadsheet:**\n * Click the \"play\" icon on the **`Start Here`** node to run it. This will create a new Google Sheet in your Google Drive named \"CVs\" with the correct columns.\n\n5. **Customize the Job Role:**\n * Go to the **`AI Qualification`** node.\n * In the **Text** parameter, find the `job_requirements` section and replace the example job description with your own. Be as detailed as possible for the best results.\n\n6. **Start Screening!**\n * Activate the workflow using the toggle at the top right.\n * Go to the **`Application Form`** node and click the **\"Open Form URL\"** button.\n * Fill out the form with a test application and upload a sample CV. Check your Google Sheet to see the AI's analysis appear within moments", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.formTrigger", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.mistralAi", - "n8n-nodes-base.googleSheets", - "@n8n/n8n-nodes-langchain.outputParserStructured", - "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", - "@n8n/n8n-nodes-langchain.chainLlm" - ], - "tags": ["trigger:formTrigger", "ai", "integration:googleSheets"], - "triggerType": "formTrigger", - "hasAI": true, - "score": 86.88, - "scoreBreakdown": { - "traction": 0.649, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.778 - }, - "source": "https://n8n.io/workflows/7456", - "author": "lucaspeyrin", - "success": true - }, - { - "id": 5541, - "slug": "track-ai-agent-token-usage-and-estimate-costs-in-google-shee-5541", - "name": "Track AI Agent token usage and estimate costs in Google Sheets", - "description": "![image.png](fileId:1659)\n### This n8n template demonstrates how to obtain token usage from AI Agents and places the data into a spreadsheet that calculates the estimated cost of the execution.\n\nObtaining the token usage from AI Agents is tricky, because it doesn't provide all the data from tool calls. This workflow taps into the workflow execution metadata to extract token usage information.\n\nWorks well with OpenAI, Google and Anthropic. Other LLM providers might need small tweaks.\n\n### How it works\n1. The AI Agent executes and then calls a subworkflow to calculate the token usage.\n2. The data is stored in Google Sheets\n3. The spreadsheet has formulas to calculate the estimated cost of the execution.\n\n### How to use\n- The AI Agent is used as an example. Feel free to replace this with other agents you have.\n- Call the subworkflow AFTER all the other branches have finished executing.\n\n### Requirements\n- LLM account (OpenAI, Gemini...) for API usage.\n- Google Drive and Sheets credentials\n- n8n API key of your instance", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.executeWorkflow", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.toolThink", - "n8n-nodes-base.set", - "n8n-nodes-base.n8n", - "n8n-nodes-base.executeWorkflowTrigger", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.summarize", - "n8n-nodes-base.googleSheets", - "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.lmChatAnthropic" - ], - "tags": ["trigger:manual", "ai", "integration:langchain"], - "triggerType": "manual", - "hasAI": true, - "score": 86.82, - "scoreBreakdown": { - "traction": 0.591, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/5541", - "author": "solomon", - "success": true - }, - { - "id": 5407, - "slug": "learn-code-node-javascript-with-an-interactive-hands-on-tuto-5407", - "name": "🎓 Learn Code Node (JavaScript) with an Interactive Hands-On Tutorial", - "description": "## How it works\n\nThis workflow is a hands-on tutorial for the **Code node** in n8n, covering both basic and advanced concepts through a simple data processing task.\n\n1. **Provides Sample Data:** The workflow begins with a sample list of users.\n2. **Processes Each Item (`Run Once for Each Item`):** The first Code node iterates through each user to calculate their `fullName` and `age`. This demonstrates basic item-by-item data manipulation using `$input.item.json`.\n3. **Fetches External Data (Advanced):** The second Code node showcases a more advanced feature. For each user, it uses the built-in `this.helpers.httpRequest` function to call an external API (genderize.io) to enrich the data with a predicted gender.\n4. **Processes All Items at Once (`Run Once for All Items`):** The third Code node receives the fully enriched list of users and runs only once. It uses `$items()` to access the entire list and calculate the `averageAge`, returning a single summary item.\n5. **Create a Binary File:** The final Code node gets the fully enriched list of users once again and creates a binary CSV file to show how to use binary data `Buffer` in JavaScript.\n\n## Set up steps\n\n**Setup time: < 1 minute**\n\nThis workflow is a self-contained tutorial and requires no setup.\n\n1. **Explore the Nodes:** Click on each of the Code nodes to read the code and the comments explaining each step, from basic to advanced.\n2. **Run the Workflow:** Click \"Execute Workflow\" to see it in action.\n3. **Check the Output:** Click on each node after the execution to see how the data is transformed at each stage. Notice how the data is progressively enriched.\n4. **Experiment!** Try changing the data in the `1. Sample Data` node, or modify the code in the Code nodes to see what happens.", - "nodes": [ - "n8n-nodes-base.set", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.code", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.manualTrigger" - ], - "tags": ["trigger:manual", "integration:code"], - "triggerType": "manual", - "hasAI": false, - "score": 86.71, - "scoreBreakdown": { - "traction": 0.692, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.571 - }, - "source": "https://n8n.io/workflows/5407", - "author": "lucaspeyrin", - "success": true - }, - { - "id": 5924, - "slug": "ai-research-assistant-via-telegram-gpt-4o-mini-deepseek-r1-s-5924", - "name": "AI Research Assistant via Telegram (GPT-4o mini + DeepSeek R1 + SerpAPI)", - "description": "## AI Research Assistant via Telegram (GPT-4o mini + DeepSeek R1 + SerpAPI)\n\n\n\n## 👥 Who’s it for\n\nThis workflow is perfect for anyone who wants to receive AI-powered research summaries directly on Telegram. Ideal for people asking frequent product, tech, or decision-making questions and want up-to-date answers sourced from the web.\n\n## 🤖 What it does\n\nUsers send a question via Telegram. An AI agent (DeepSeek R1) reformulates and understands the intent, while a second agent (GPT-4o mini) performs live research using SerpAPI. The most relevant answers, including links and images, are delivered back via Telegram.\n\n \n## ⚙️ How it works\n\n📲 Telegram Trigger – Starts when a user sends a message to your Telegram bot.\n🧠 DeepSeek R1 Agent – Understands, clarifies, or reformulates the user query.\n🧠 Research AI Agent (GPT-4o mini + SerpAPI) – Searches the web and summarizes the best results.\n📤 Send Telegram Message – Sends the response back to the same user.\n\n## 📋 Requirements\n\n- Telegram bot (via BotFather) with API token set in n8n credentials\n\n- OpenAI account with API key and balance for GPT-4o mini\n\n- SerpAPI account (100 free searches/month) with API key\n\n- DeepSeek account with API key and balance\n\n\n\n## 🛠️ How to set up\n\nCreate your Telegram bot using BotFather and connect it using the Telegram Trigger node\nSet up DeepSeek credentials and add a Chat Model AI Agent node using DeepSeek R1 to reformulate the user’s question\nSet up OpenAI credentials and add a second ChatGPT AI Agent node using GPT-4o mini\nIn the GPT-4o node, enable the SerpAPI Tool and add your SerpAPI API key\nPass the reformulated question from DeepSeek to the GPT-4o agent for live search and summarization\nFormat the response (text, links, optional images)\nSend the final reply to the user using the Telegram Send Message node\nEnsure your n8n instance is publicly accessible\nTest the workflow by sending a message to your Telegram bot ✅\n", - "nodes": [ - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.lmChatDeepSeek", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.toolSerpApi", - "n8n-nodes-base.telegramTrigger", - "n8n-nodes-base.telegram", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:telegram", "ai", "integration:langchain"], - "triggerType": "telegram", - "hasAI": true, - "score": 86.62, - "scoreBreakdown": { - "traction": 0.612, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.875 - }, - "source": "https://n8n.io/workflows/5924", - "author": "arlindeveloper", - "success": true - }, - { - "id": 4557, - "slug": "intelligent-email-organization-with-ai-powered-content-class-4557", - "name": "Intelligent Email Organization with AI-Powered Content Classification for Gmail", - "description": "This workflow leverages AI to intelligently analyze incoming Gmail messages and automatically apply relevant labels based on the email content.\n\nThe default configuration includes the following labels:\n\n* **Newsletter**: Subscription updates or promotional content.\n* **Inquiry**: Emails requesting information or responses.\n* **Invoice**: Billing and payment-related emails.\n* **Proposal**: Business offers or collaboration opportunities.\n* **Action Required**: Emails demanding immediate tasks or actions.\n* **Follow-up Reminder**: Emails prompting follow-up actions.\n* **Task**: Emails containing actionable tasks.\n* **Personal**: Non-work-related emails.\n* **Urgent**: Time-sensitive or critical communications.\n* **Bank**: Banking alerts and financial statements.\n* **Job Update**: Recruitment or job-related communications.\n* **Spam/Junk**: Unwanted or irrelevant bulk emails.\n* **Social/Networking**: Notifications from social platforms.\n* **Receipt**: Purchase confirmations and receipts.\n* **Event Invite**: Invitations or calendar-related messages.\n* **Subscription Renewal**: Reminders for subscription expirations.\n* **System Notification**: Technical alerts from services or systems.\n\nYou can customize labels and definitions based on your specific use case.\n\n### How it works:\n\n* The workflow periodically retrieves new Gmail messages.\n* Only emails without existing labels, regardless of read status, are sent to the AI for analysis.\n* Email content (subject and body) is analyzed by an AI model to determine the appropriate label.\n* Labels identified by the AI are applied to each email accordingly.\n\n**Note:** This workflow performs 100% better than the default Gmail trigger method, which is why the workflow was switched from Gmail trigger to a scheduled workflow. By selectively processing only unlabeled emails, it ensures comprehensive labeling while significantly reducing AI processing costs.\n\n### Setup Steps:\n\n1. Configure credentials for Gmail and your chosen AI service (e.g., OpenAI).\n2. Ensure labels exist in your Gmail account matching the workflow definitions.\n3. Adjust the AI prompt to match your labeling needs.\n4. Optionally customize the polling interval (default: every 2 minutes).\n\nThis workflow streamlines your email management, keeping your inbox organized effortlessly while optimizing resource usage.\n", - "nodes": [ - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.noOp", - "n8n-nodes-base.if", - "n8n-nodes-base.merge", - "n8n-nodes-base.gmail", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.set", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.compareDatasets", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.code" - ], - "tags": ["trigger:schedule", "ai", "integration:gmail"], - "triggerType": "schedule", - "hasAI": true, - "score": 86.6, - "scoreBreakdown": { - "traction": 0.668, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.647 - }, - "source": "https://n8n.io/workflows/4557", - "author": "niranjan", - "success": true - }, - { - "id": 3016, - "slug": "invoices-from-gmail-to-drive-and-google-sheets-3016", - "name": "Invoices from Gmail to Drive and Google Sheets", - "description": "# Attachments Gmail to Drive and Google Sheets\n\n## Description\nAutomatically process invoice emails by saving attachments to Google Drive and extracting key invoice data to Google Sheets using AI. This workflow monitors your Gmail for unread emails with attachments, saves PDFs to a specified Google Drive folder, and uses OpenAI's GPT-4o to extract invoice details (date, description, amount) into a structured spreadsheet.\n\n## Use cases\n- **Invoice Management**: Automatically organize and track invoices received via email\n- **Financial Record Keeping**: Maintain a structured database of all invoice information\n- **Document Organization**: Keep digital copies of invoices organized in Google Drive\n- **Automated Data Entry**: Eliminate manual data entry for invoice processing\n\n## Resources\n- Gmail account\n- Google Drive account\n- Google Sheets account\n- OpenAI API key\n\n## Setup instructions\n\n### Prerequisites\n1. Active Gmail, Google Drive, and Google Sheets accounts\n2. OpenAI API key (GPT-4o model access)\n3. n8n instance with credentials manager\n\n### Steps\n1. **Gmail and Google Drive Setup**:\n - Connect your Gmail account in n8n credentials\n - Connect your Google Drive account with appropriate permissions\n - Create a destination folder in Google Drive for invoice storage\n\n2. **Google Sheets Setup**:\n - Connect your Google Sheets account\n - Create a spreadsheet with columns: Invoice date, Invoice Description, Total price, and Fichero\n - Copy your spreadsheet ID for configuration\n\n3. **OpenAI Setup**:\n - Add your OpenAI API key to n8n credentials\n\n4. **Configure Email Filter**:\n - Update the email filter node to match your specific sender requirements\n\n## Benefits\n- **Time Saving**: Eliminates manual downloading, filing, and data entry\n- **Accuracy**: AI-powered data extraction reduces human error\n- **Organization**: Consistent file naming and storage structure\n- **Searchability**: Creates a searchable database of all invoice information\n- **Automation**: Runs every minute to process new emails as they arrive\n\n## Related templates\n- Email Parser to CRM\n- Document Processing Workflow\n- Financial Data Automation", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.gmailTrigger", - "n8n-nodes-base.set", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.googleDrive", - "n8n-nodes-base.gmail", - "n8n-nodes-base.extractFromFile", - "@n8n/n8n-nodes-langchain.lmOpenAi", - "@n8n/n8n-nodes-langchain.outputParserStructured", - "n8n-nodes-base.googleSheets", - "@n8n/n8n-nodes-langchain.chainLlm", - "n8n-nodes-base.if" - ], - "tags": ["trigger:gmail", "ai", "integration:googleDrive"], - "triggerType": "gmail", - "hasAI": true, - "score": 86.59, - "scoreBreakdown": { - "traction": 0.633, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.786 - }, - "source": "https://n8n.io/workflows/3016", - "author": "carlosgracia", - "success": true - }, - { - "id": 5024, - "slug": "build-custom-workflows-automatically-with-gpt-4o-rag-and-web-5024", - "name": "Build Custom Workflows Automatically with GPT-4o, RAG, and Web Search", - "description": "## 🚀 What the “Agent Builder” template does\n\nNeed to **turn a one-line chat request into a fully-wired n8n workflow template—complete with AI agents, RAG, and web-search super-powers—without lifting a finger?**\nThat’s exactly what *Agent Builder* automates:\n\n1. **Listens to any incoming chat message** (via the *Chat Trigger*).\n2. **Spins up an AI architect** that analyses the request, searches the web, reads n8n docs from a Pinecone vector store, and designs the smallest possible set of nodes.\n3. **Auto-generates a ready-to-import JSON template** and hands it back as a downloadable file—plus all the supporting assets (embeddings, vector store etc.) so the next prompt is even smarter.\n\nThink of it as your personal “workflow chef”: you shout the order, it shops for ingredients, cooks, plates, and serves the meal. All you do is eat.\n\n---\n\n## 🤗 Who will love this?\n\n* **No-code builders / power users** who don’t want to wrestle with AI node wiring.\n* **Agencies & consultants** delivering lots of bespoke automations.\n* **Internal platform teams** who need a “workflow self-service portal” for non-technical colleagues.\n\n---\n\n## 🧩 How it’s wired\n\n| Sub-process | What happens inside | Key nodes |\n| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |\n| **Web Crawler** (optional) | Firecrawl scrapes docs.n8n.io (or any URL you drop in) and streams raw markdown back. | `Set URL → HTTP Request (Extract) → Wait & Retry` |\n| **RAG Trainer** | Splits the scraped docs, embeds them with OpenAI, and upserts vectors into Pinecone. | `Recursive Text Splitter → Embeddings OpenAI → Train Pinecone` |\n| **Agent Builder** | The star of the show – orchestrates GPT-4o (via OpenRouter), SerpAPI web-search, your Pinecone index and a Structured Output Parser to **produce → validate → prettify** the final n8n template. | `Chat Trigger → AI Agent → OpenAI (validator) → Code (extract) → Convert to JSON file` |\n\n*Every arrow in the drawn workflow is pre-connected, so the generated template always passes n8n’s import check.*\n\n---\n\n## 🛠️ Getting set up (5 quick creds)\n\n| Service | Credential type |\n| --------------------------------------------------- | ---------------------------------------------------------- |\n| **OpenAI / Azure OpenAI** – embeddings & validation | *OpenAI API* |\n| **Pinecone** – vector store | *Pinecone API* |\n| **OpenRouter** – GPT-4o LLM | *OpenRouter API Key* |\n| **SerpAPI** – web search | *SerpAPI Key* |\n| **Firecrawl** (only if you plan to crawl) | Generic **Header Auth** → `Authorization: Bearer YOUR_KEY` |\n\nEach node already expects those creds; just create them once, select in the dropdown, hit **Activate**.\n\n---\n\n## 🏃‍♀️ What a typical run looks like\n\n1. **User says:** “Build me a workflow that monitors our support inbox, summarises new tickets with GPT and posts to Slack.”\n2. *Chat Trigger* captures the message.\n3. **AI Agent**:\n\n * queries Pinecone for relevant n8n docs,\n * fires a SerpAPI search for “n8n gmail trigger example”,\n * sketches an architecture (Gmail Trigger → GPT Model → Slack).\n4. The agent **returns JSON** ➜ OpenAI node double-checks field names, connections, type versions.\n5. A tiny JS Code node slices the JSON out of the chat blob and **saves it as `template.json`** ready for download.\n6. You download, import, and… done.\n\n---\n\n## ✏️ Customising\n\n* **Switch the LLM** – plug in Claude 3, Gemini 1.5, or a local model; just swap the *OpenRouter Chat Model* node.\n* **Point the RAG at your own docs** – change the crawl URL or feed PDFs via the *Default Data Loader*.\n* **Hard-code preferred nodes** – edit the “User node preferences” in the system message so the agent always chooses *Notion* for databases, etc.\n\n---\n\n## 🥡 Take-away notes\n\n* It's a **prototype** feel free to experiment with it to improve its capabilities.\n* **Have fun building!**\n", - "nodes": [ - "@n8n/n8n-nodes-langchain.chatTrigger", - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.embeddingsOpenAi", - "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.if", - "n8n-nodes-base.wait", - "@n8n/n8n-nodes-langchain.toolSerpApi", - "@n8n/n8n-nodes-langchain.vectorStorePinecone", - "@n8n/n8n-nodes-langchain.lmChatOpenRouter", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.code", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.set", - "n8n-nodes-base.convertToFile", - "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter" - ], - "tags": ["trigger:chatTrigger", "ai", "integration:langchain"], - "triggerType": "chatTrigger", - "hasAI": true, - "score": 86.56, - "scoreBreakdown": { - "traction": 0.654, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.696 - }, - "source": "https://n8n.io/workflows/5024", - "author": "agents-by-franz", - "success": true - }, - { - "id": 3086, - "slug": "publish-wordpress-posts-to-social-media-x-facebook-linkedin--3086", - "name": "Publish WordPress Posts to Social Media X, Facebook, LinkedIn, Instagram with AI", - "description": "### Workflow Description for n8n: Social Media Post from Ideas Copy\n\nThis workflow automates the process of creating and publishing **social media posts** across multiple platforms (Twitter/X, Facebook, LinkedIn, and Instagram) based on content from a WordPress post. It uses **AI models** to generate platform-specific captions and images, and integrates with **Google Sheets**, **WordPress**, and various social media APIs. \n\nIs a powerful tool for automating social media post creation and publishing, saving time and ensuring consistent, platform-optimized content across multiple channels.\n\nBelow is a breakdown of the workflow:\n\n---\n\n#### **1. How It Works**\nThe workflow is designed to streamline the creation and publishing of social media posts. Here's how it works:\n\n1. **Trigger**:\n - The workflow starts with a **Manual Trigger** node, which initiates the process when the user clicks \"Test workflow.\"\n\n2. **Fetch Data**:\n - The **Google Sheets** node retrieves the **WordPress Post ID** from a predefined Google Sheet.\n - The **Get Post** node fetches the corresponding WordPress post content using the retrieved Post ID.\n\n3. **Generate Social Media Content**:\n - The **Social Media Manager** node uses an **AI model (OpenRouter)** to analyze the WordPress post and generate platform-specific captions for **Twitter/X**, **Facebook**, **LinkedIn**, and **Instagram**.\n - The AI ensures that each caption is tailored to the platform's audience, tone, and style, including hashtags and calls to action.\n\n4. **Generate Images**:\n - The **Image Instagram** and **Image Facebook e Linkedin** nodes use **OpenAI** to generate platform-specific images for Instagram, Facebook, and LinkedIn posts.\n\n5. **Publish on Social Media**:\n - The workflow publishes the generated content and images on the respective platforms:\n - **Publish on X**: Posts the caption on Twitter/X.\n - **Publish on LinkedIn**: Posts the caption and image on LinkedIn.\n - **Publish on Facebook**: Posts the caption and image on Facebook.\n - **Publish on Instagram**: Posts the caption and image on Instagram.\n\n6. **Update Google Sheets**:\n - The workflow updates the Google Sheet to mark the posts as published (e.g., \"X OK,\" \"Facebook OK,\" \"LinkedIn OK,\" \"Instagram OK\").\n\n---\n\n#### **2. Set Up Steps**\nTo set up and use this workflow in n8n, follow these steps:\n\n1. **Google Sheets Setup**:\n - Create (or clone) a Google Sheet with columns for **POST ID**, **TEXT**, **TWITTER**, **FACEBOOK**, **INSTAGRAM**, and **LINKEDIN**.\n - Link the Google Sheet to the **Google Sheets** node by providing the **Document ID** and **Sheet Name**.\n\n2. **WordPress Integration**:\n - Set up WordPress credentials in n8n for the **Get Post** node.\n - Ensure the WordPress site is accessible via its REST API.\n\n3. **AI Model Configuration**:\n - Configure the **OpenRouter** credentials in n8n for the **Social Media Manager** node.\n - Ensure the AI model is set up to generate platform-specific captions.\n\n4. **Image Generation**:\n - Set up **OpenAI** credentials for the **Image Instagram** and **Image Facebook e Linkedin** nodes to generate images.\n\n5. **Social Media API Integration**:\n - Set up credentials for each social media platform:\n - **Twitter/X**: Configure the **Publish on X** node with Twitter OAuth2 credentials.\n - **LinkedIn**: Configure the **Publish on LinkedIn** node with LinkedIn OAuth2 credentials.\n - **Facebook**: Configure the **Publish on Facebook** and **Publish on Instagram** nodes with Facebook Graph API credentials.\n\n6. **Test the Workflow**:\n - Click the **\"Test workflow\"** button in n8n to trigger the workflow.\n - The workflow will:\n - Fetch the WordPress post content.\n - Generate platform-specific captions and images.\n - Publish the posts on Twitter/X, Facebook, LinkedIn, and Instagram.\n - Update the Google Sheet to mark the posts as published.\n\n7. **Optional Customization**:\n - Modify the workflow to include additional platforms or customize the AI-generated content further.\n\n\n---\n\n\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/). ", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.googleSheets", - "@n8n/n8n-nodes-langchain.lmChatOpenRouter", - "@n8n/n8n-nodes-langchain.outputParserStructured", - "@n8n/n8n-nodes-langchain.openAi", - "@n8n/n8n-nodes-langchain.chainLlm", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.wordpress", - "n8n-nodes-base.twitter", - "n8n-nodes-base.linkedIn", - "n8n-nodes-base.facebookGraphApi", - "n8n-nodes-base.httpRequest" - ], - "tags": ["trigger:manual", "ai", "integration:googleSheets"], - "triggerType": "manual", - "hasAI": true, - "score": 86.55, - "scoreBreakdown": { - "traction": 0.655, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.688 - }, - "source": "https://n8n.io/workflows/3086", - "author": "n3witalia", - "success": true - }, - { - "id": 4400, - "slug": "build-a-pdf-document-rag-system-with-mistral-ocr-qdrant-and--4400", - "name": "Build a PDF Document RAG System with Mistral OCR, Qdrant and Gemini AI", - "description": "This workflow is designed to **process PDF documents** using **Mistral's OCR** capabilities, store the extracted text in a Qdrant vector database, and enable Retrieval-Augmented Generation (**RAG**) for answering questions. Here’s how it functions: \n\nOnce configured, the workflow automates document ingestion, vectorization, and intelligent querying, enabling powerful RAG applications.\n\n---\n\n### **Benefits**\n\n* **End-to-End Automation**\n No manual interaction is needed: documents are read, processed, and made queryable with minimal setup.\n\n* **Scalable and Modular**\n The workflow uses subflows and batching, making it easy to scale and customize.\n\n* **Multi-Model Support**\n Combines Mistral for OCR, OpenAI for embeddings, and Gemini for intelligent answering—taking advantage of the strengths of each.\n\n* **Real-Time Q\\&A**\n With RAG integration, users can query document content through natural language and receive accurate responses grounded in the PDF data.\n\n* **Light or Full Mode**\n Users can choose to index full page content or only summarized text, optimizing for either performance or richness.\n\n---\n\n### **How It Works** \n\n1. **PDF Processing with Mistral OCR**: \n - The workflow starts by uploading a PDF file to Mistral's API, which performs OCR to extract text and metadata. \n - The extracted content is split into manageable chunks (e.g., pages or sections) for further processing. \n\n2. **Vector Storage in Qdrant**: \n - The extracted text is converted into embeddings using OpenAI's embedding model. \n - These embeddings are stored in a Qdrant vector database, enabling efficient similarity searches for RAG. \n\n3. **Question-Answering with RAG**: \n - When a user submits a question via a chat interface, the workflow retrieves relevant text chunks from Qdrant using vector similarity. \n - A language model (Google Gemini) generates answers based on the retrieved context, providing accurate and context-aware responses. \n\n4. **Optional Summarization**: \n - The workflow includes an optional summarization step using Google Gemini to condense the extracted text for faster processing or lighter RAG usage. \n\n---\n\n### **Set Up Steps** \nTo deploy this workflow in n8n, follow these steps: \n\n1. **Configure Qdrant Database**: \n - Replace `QDRANTURL` and `COLLECTION` in the \"Create collection\" and \"Refresh collection\" nodes with your Qdrant instance details. \n - Ensure the Qdrant collection is configured with the correct vector size (e.g., 1536 for OpenAI embeddings) and distance metric (e.g., Cosine). \n\n2. **Set Up Credentials**: \n - Add credentials for: \n - **Mistral Cloud API** (for OCR processing). \n - **OpenAI API** (for embeddings). \n - **Google Gemini API** (for chat and summarization). \n - **Google Drive** (if sourcing PDFs from Drive). \n - **Qdrant API** (for vector storage). \n\n3. **PDF Source Configuration**: \n - If using Google Drive, specify the folder ID in the \"Search PDFs\" node. \n - Alternatively, modify the workflow to accept PDFs from other sources (e.g., direct uploads or external APIs). \n\n4. **Customize Text Processing**: \n - Adjust chunk size and overlap in the \"Token Splitter\" node to optimize for your document type. \n - Choose between raw text or summarized content for RAG by toggling between the \"Set page\" and \"Summarization Chain\" nodes. \n\n5. **Test the RAG**: \n - Trigger the workflow manually or via a chat message to verify OCR, embedding, and Qdrant storage. \n - Use the \"Question and Answer Chain\" node to test query responses. \n\n6. **Optional Sub-Workflows**: \n - The workflow supports execution as a sub-workflow for batch processing (e.g., handling multiple PDFs). \n\n----\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/). ", - "nodes": [ - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.splitInBatches", - "@n8n/n8n-nodes-langchain.embeddingsOpenAi", - "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", - "@n8n/n8n-nodes-langchain.textSplitterTokenSplitter", - "@n8n/n8n-nodes-langchain.chatTrigger", - "@n8n/n8n-nodes-langchain.chainRetrievalQa", - "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", - "@n8n/n8n-nodes-langchain.retrieverVectorStore", - "@n8n/n8n-nodes-langchain.vectorStoreQdrant", - "n8n-nodes-base.code", - "n8n-nodes-base.wait", - "n8n-nodes-base.executeWorkflow", - "n8n-nodes-base.executeWorkflowTrigger", - "n8n-nodes-base.set", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.chainSummarization", - "n8n-nodes-base.googleDrive" - ], - "tags": ["trigger:manual", "ai", "integration:langchain"], - "triggerType": "manual", - "hasAI": true, - "score": 86.5, - "scoreBreakdown": { - "traction": 0.67, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.621 - }, - "source": "https://n8n.io/workflows/4400", - "author": "n3witalia", - "success": true - }, - { - "id": 3904, - "slug": "search-linkedin-companies-score-with-ai-and-add-them-to-goog-3904", - "name": "Search LinkedIn companies, Score with AI and add them to Google Sheet CRM", - "description": "# Search LinkedIn companies, Score with AI and add them to Google Sheet CRM\n\n**Setup Video: [https://youtube.com/watch?v=m904RNxtF0w&t](https://youtube.com/watch?v=m904RNxtF0w&t)**\n\n### Who is this for?\nThis template is ideal for sales teams, business development professionals, and marketers looking to build a targeted prospect database with automatic qualification. Perfect for agencies, consultants, and B2B companies wanting to identify and prioritize the most promising potential clients.\n\n### What problem does this workflow solve?\nManually researching companies on LinkedIn, evaluating their fit for your services, and tracking them in your CRM is time-consuming and subjective. This automation streamlines lead generation by automatically finding, scoring, and importing qualified prospects into your database.\n\n### What this workflow does\nThis workflow automatically searches for companies on LinkedIn based on your criteria, retrieves detailed information about each company, filters them based on quality indicators, uses AI to score how well they match your ideal customer profile, and adds them to your Google Sheet CRM while preventing duplicates.\n\n### Setup\n1. Create a Ghost Genius API account and get your API key\n2. Configure HTTP Request nodes with Header Auth credentials\n3. Create a copy of the provided Google Sheet template\n4. Set up your Google Sheet and OpenAI credentials following n8n documentation\n5. Customize the \"Set Variables\" node to match your target audience and scoring criteria\n\n### How to customize this workflow\n- Modify search parameters to target different industries, locations, or company sizes\n- Adjust the follower count threshold based on your qualification criteria\n- Customize the AI scoring system to align with your specific product or service offering\n- Add notification nodes to alert you when high-scoring companies are identified", - "nodes": [ - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.if", - "n8n-nodes-base.set", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.googleSheets", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.wait", - "n8n-nodes-base.manualTrigger" - ], - "tags": ["trigger:manual", "ai", "integration:httpRequest"], - "triggerType": "manual", - "hasAI": true, - "score": 86.47, - "scoreBreakdown": { - "traction": 0.636, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.75 - }, - "source": "https://n8n.io/workflows/3904", - "author": "yaznow", - "success": true - }, - { - "id": 2894, - "slug": "upload-to-instagram-tiktok-youtube-from-google-drive-2894", - "name": "Upload to Instagram, TikTok & YouTube from Google Drive", - "description": "## Description\nThis automation template is designed for content creators, digital marketers, and social media managers looking to simplify their video posting workflow. It automates the process of generating engaging video descriptions and uploading content to both Instagram and TikTok, making your social media management more efficient and error-free.\n\n### Who Is This For?\n- **Content Creators & Influencers:** Streamline your video uploads and focus more on creating content.\n- **Digital Marketers:** Ensure consistent posting across multiple platforms with minimal manual intervention.\n- **Social Media Managers:** Automate repetitive tasks and maintain a steady online presence.\n\n### What Problem Does This Workflow Solve?\nManually creating descriptions and uploading videos to different platforms can be time-consuming and error-prone. This workflow addresses these challenges by:\n- **Automating Video Uploads:** Monitors a designated Google Drive folder for new videos.\n- **Generating Descriptions:** Uses OpenAI to transcribe video audio and generate engaging, customized social media descriptions.\n- **Ensuring Multi-Platform Consistency:** Simultaneously posts your video with the generated description to Instagram and TikTok.\n- **Error Notifications:** Optional Telegram integration sends alerts in case of issues, ensuring smooth operations.\n\n### How It Works\n1. **Video Upload:** Place your video in the designated Google Drive folder.\n2. **Description Generation:** The automation triggers OpenAI to transcribe your video’s audio and generate a captivating description.\n3. **Content Distribution:** Automatically uploads the video and description to both Instagram and TikTok.\n4. **Error Handling:** Sends Telegram notifications if any issues arise during the process.\n\n### Setup\n1. Generate an API token at [social media API upload-post](https://upload-post.com) and configure it in both the Upload to TikTok and Upload to Instagram nodes.\n2. **Google Cloud Project:** Create a project in Google Cloud Platform, enable the Google Drive API, and generate the necessary OAuth credentials to connect to your Google Drive account.\n3. Set up your Google Drive folder in the Google Drive Trigger node.\n4. Customize the OpenAI prompt in the Generate Social Description node to match your brand’s tone.\n5. (Optional) Configure Telegram credentials for error notifications.\n\n### Requirements\n- **Accounts:** upload-post.com, Google Drive, and (optionally) Telegram.\n- **API Keys & Credentials:** Upload-post.com API token, OpenAI API key, and (optional) Telegram bot token.\n- **Google Cloud:** A project with the Google Drive API enabled and valid OAuth credentials.\n\nUse this template to enhance your productivity, maintain consistency across your social media channels, and engage your audience with high-quality video content.\n", - "nodes": [ - "n8n-nodes-base.googleDriveTrigger", - "n8n-nodes-base.googleDrive", - "n8n-nodes-base.errorTrigger", - "n8n-nodes-base.telegram", - "n8n-nodes-base.if", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.writeBinaryFile", - "n8n-nodes-base.readBinaryFile", - "n8n-nodes-base.httpRequest" - ], - "tags": ["trigger:other", "ai", "integration:readBinaryFile"], - "triggerType": "other", - "hasAI": true, - "score": 86.47, - "scoreBreakdown": { - "traction": 0.663, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.643 - }, - "source": "https://n8n.io/workflows/2894", - "author": "carlosgracia", - "success": true - }, - { - "id": 4376, - "slug": "extract-invoice-data-from-email-to-google-sheets-using-gpt-4-4376", - "name": "Extract Invoice Data from Email to Google Sheets using GPT-4o AI Automation", - "description": "\nTransform your invoice processing from manual data entry into an intelligent automation system. This powerful n8n workflow monitors Gmail for invoice attachments, extracts data using AI-powered analysis, and creates organized Google Sheets with all relevant financial information automatically structured and ready for your accounting workflows.\n\n## 🔄 How It Works\n\nThis sophisticated 8-step automation eliminates manual invoice processing:\n\n**Step 1: Intelligent Email Monitoring**\nThe workflow continuously monitors your Gmail account for emails with specific labels, checking every minute for new invoice attachments that need processing.\n\n**Step 2: Attachment Verification**\nSmart filtering ensures only emails with PDF attachments are processed, preventing unnecessary workflow triggers from text-only emails.\n\n**Step 3: Advanced PDF Extraction**\nThe system automatically downloads and converts PDF invoices into readable text, handling various invoice formats and layouts with high accuracy.\n\n**Step 4: AI-Powered Data Analysis**\nGPT-4 processes the extracted text using specialized prompts designed for financial document analysis, identifying and extracting:\n- Company information and contact details\n- Invoice numbers, dates, and payment terms\n- Detailed line items with quantities and pricing\n- Tax calculations including CGST, SGST, and VAT\n- Billing and shipping addresses\n- Payment methods and transaction references\n\n**Step 5: Structured Data Formatting**\nThe AI output is automatically formatted into clean, consistent JSON structure with 25+ standardized fields for comprehensive invoice tracking.\n\n**Step 6: Dynamic Spreadsheet Creation**\nEach processed invoice generates a new Google Sheets document with timestamp naming and organized data layout, ready for accounting review.\n\n**Step 7: Automated File Organization**\nProcessed spreadsheets are automatically moved to designated Google Drive folders, maintaining organized file structure for easy retrieval and audit trails.\n\n**Step 8: Data Population**\nAll extracted invoice data is populated into the spreadsheet with proper formatting, formulas, and structure for immediate use in accounting workflows.\n\n## ⚙️ Setup Steps\n\n### Prerequisites\n- Gmail account with invoice-receiving capability\n- Google Workspace access for Sheets and Drive\n- OpenAI API account for data extraction\n- n8n instance (cloud or self-hosted)\n- PDF invoices (text-based, not scanned images)\n\n### Gmail Configuration Requirements\n\n**Label Setup:**\nCreate specific Gmail labels for invoice processing:\n```\n📧 Labels to Create:\n- \"Invoice-Processing\" (main processing label)\n- \"Invoice-Vendors\" (supplier invoices)\n- \"Invoice-Clients\" (customer invoices) \n- \"Invoice-Processed\" (completed items)\n```\n\n**Email Filter Configuration:**\nSet up automatic labeling rules:\n- Emails from known vendors → Auto-apply \"Invoice-Processing\"\n- Emails with \"Invoice\" in subject → Auto-apply \"Invoice-Processing\" \n- Attachments with PDF extension → Auto-apply \"Invoice-Processing\"\n\n### Configuration Steps\n\n**1. Credential Setup**\n- **Gmail OAuth2**: Full email access including attachments\n- **OpenAI API Key**: GPT-4 access for intelligent data extraction\n- **Google Sheets OAuth2**: Spreadsheet creation and editing permissions\n- **Google Drive OAuth2**: File organization and folder management\n\n**2. Google Drive Folder Structure**\nCreate organized folder hierarchy:\n```\n📁 Invoice Management/\n├── 📁 Incoming Invoices/\n├── 📁 Processed Invoices/\n│ ├── 📁 2024/\n│ │ ├── 📁 Q1/\n│ │ ├── 📁 Q2/\n│ │ └── 📁 Q3/\n├── 📁 Vendor Invoices/\n└── 📁 Client Invoices/\n```\n\n**3. AI Extraction Customization**\nThe default AI prompt extracts standard invoice fields but can be customized for:\n- **Regional Tax Systems**: GST (India), VAT (EU), Sales Tax (US)\n- **Industry-Specific Fields**: Purchase orders, project codes, cost centers\n- **Company Standards**: Custom fields, approval workflows, coding requirements\n- **Multi-Currency**: Exchange rates, currency conversion, international invoices\n\n**4. Data Validation Rules**\nImplement quality control measures:\n- **Required Field Validation**: Ensure critical data is always extracted\n- **Format Standardization**: Consistent date formats, number formatting\n- **Duplicate Detection**: Identify potentially duplicate invoices\n- **Accuracy Scoring**: Confidence levels for extracted data\n\n**5. Workflow Activation**\n- Import the workflow JSON into your n8n instance\n- Configure all credential connections and test each step\n- Process test invoices to verify accuracy\n- Activate Gmail trigger for continuous monitoring\n\n## 🚀 Use Cases\n\n### **Accounting Firms & Bookkeepers**\n- **Client Service Automation**: Process invoices for multiple clients efficiently\n- **Data Entry Elimination**: Convert hours of manual work into automated processing\n- **Accuracy Improvement**: Reduce human errors in financial data transcription\n- **Scalable Operations**: Handle increased client volume without proportional staff increase\n\n### **Small & Medium Businesses**\n- **Accounts Payable Automation**: Streamline vendor invoice processing\n- **Cash Flow Management**: Quick access to payment due dates and amounts\n- **Expense Tracking**: Organized categorization of business expenses\n- **Audit Preparation**: Maintain organized, searchable invoice records\n\n### **Corporate Finance Teams**\n- **Procurement Processing**: Handle purchase orders and vendor invoices at scale\n- **Multi-Location Operations**: Centralize invoice processing across offices\n- **Compliance Management**: Ensure consistent data capture for regulatory requirements\n- **Integration Readiness**: Prepare data for ERP and accounting system import\n\n### **Freelancers & Consultants**\n- **Client Invoice Tracking**: Organize incoming payments and project billing\n- **Expense Management**: Categorize business expenses for tax preparation\n- **Cash Flow Monitoring**: Track outstanding invoices and payment schedules\n- **Professional Organization**: Maintain clean financial records for business growth\n\n### **E-commerce & Retail**\n- **Supplier Invoice Processing**: Manage inventory purchasing and cost tracking\n- **Multi-Vendor Operations**: Handle invoices from numerous suppliers efficiently\n- **Cost Analysis**: Track product costs and supplier performance\n- **Inventory Reconciliation**: Match invoice data with purchase orders and receipts\n\n## 🔧 Advanced Customization Options\n\n### **Multi-Format Invoice Handling**\nExtend processing capabilities:\n```\n- PDF Text-Based: Standard invoice PDFs with selectable text\n- Scanned Documents: Add OCR processing for image-based invoices\n- Email Body Invoices: Extract data from invoice details in email content \n- Excel Attachments: Process invoices sent as spreadsheet files\n- Multi-Page Documents: Handle complex invoices with multiple pages\n```\n\n### **Intelligent Data Validation**\nImplement quality assurance features:\n- **Cross-Reference Validation**: Compare extracted data against purchase orders\n- **Vendor Database Matching**: Verify company details against known vendor lists\n- **Tax Calculation Verification**: Validate tax amounts and rates for accuracy\n- **Currency Conversion**: Handle multi-currency invoices with real-time exchange rates\n\n### **Workflow Integration Extensions**\nConnect to existing business systems:\n- **ERP Integration**: Direct data export to SAP, Oracle, or Microsoft Dynamics\n- **Accounting Software**: Push data to QuickBooks, Xero, or FreshBooks\n- **Approval Workflows**: Add review and approval steps before final processing\n- **Payment Processing**: Connect to banking systems for automated payment scheduling\n\n### **Advanced Analytics & Reporting**\nGenerate business insights:\n- **Vendor Performance Analysis**: Track pricing trends and payment terms\n- **Expense Category Reporting**: Automated expense categorization and analysis\n- **Cash Flow Forecasting**: Predict payment obligations based on due dates\n- **Audit Trail Management**: Maintain comprehensive processing logs for compliance\n\n## 📊 Extracted Data Structure\n\n### **Standard Invoice Fields (25+ Data Points)**\nThe AI extraction captures comprehensive invoice information:\n\n**Header Information:**\n- Billed To (Customer/Company Name)\n- Invoice Number (Unique Identifier)\n- Date of Issue (Invoice Creation Date)\n- Due Date (Payment Deadline)\n\n**Line Item Details:**\n- Item Description (Product/Service Details)\n- Quantity (Number of Items/Hours)\n- Rate (Unit Price)\n- Amount (Line Total)\n\n**Tax and Financial Calculations:**\n- CGST/SGST Rates and Amounts (Indian GST System)\n- VAT Calculations (European Tax System)\n- Subtotal (Pre-tax Amount)\n- Total Amount (Final Invoice Value)\n\n**Company and Contact Information:**\n- Vendor Company Name\n- Contact Phone/Mobile\n- Email Address\n- Website URL\n- GST Registration Number\n- PAN Number (Indian Tax ID)\n\n**Address Information:**\n- Billing Address\n- Shipping Address \n- Place of Supply\n- Place of Delivery\n\n**Payment Details:**\n- Transaction IDs\n- Payment Mode (Check, Bank Transfer, Card)\n- Terms and Conditions\n- Special Instructions\n\n### **Sample Extracted Data:**\n```json\n{\n \"billed_to\": \"Tech Solutions Inc.\",\n \"invoice_number\": \"INV-2024-0156\",\n \"date_of_issue\": \"2024-03-15\",\n \"due_date\": \"2024-04-15\",\n \"item_0_description\": \"Web Development Services\",\n \"item_0_quantity\": 40,\n \"item_0_rate\": 75.00,\n \"item_0_amount\": 3000.00,\n \"tax_0_cgst_rate\": 9,\n \"tax_0_cgst_amount\": 270.00,\n \"tax_0_sgst_rate\": 9,\n \"tax_0_sgst_amount\": 270.00,\n \"subtotal\": 3000.00,\n \"total\": 3540.00,\n \"company_name\": \"Digital Services LLC\",\n \"company_email\": \"billing@digitalservices.com\",\n \"payment_transaction_ids\": \"TXN123456789\",\n \"mode_of_payment\": \"Bank Transfer\"\n}\n```\n\n## 🛠️ Troubleshooting & Best Practices\n\n### **Common Issues & Solutions**\n\n**PDF Extraction Challenges**\n- **Scanned Documents**: Original workflow handles text-based PDFs only\n- **Complex Layouts**: Some invoice formats may require prompt refinement\n- **Multi-Page Invoices**: Large invoices might need pagination handling\n- **Password Protection**: Encrypted PDFs require manual processing\n\n**AI Extraction Accuracy**\n- **Field Recognition**: Some custom invoice formats may need prompt tuning\n- **Currency Handling**: Multi-currency invoices may require specific configuration\n- **Date Formats**: International date formats might need standardization\n- **Vendor Variations**: Different vendor invoice styles may affect accuracy\n\n**Gmail Integration Limitations**\n- **Label Management**: Ensure consistent labeling for proper processing\n- **Attachment Size**: Large PDFs may hit Gmail API limits\n- **Email Volume**: High-volume processing may require rate limiting\n- **Security Settings**: Corporate Gmail may have additional restrictions\n\n### **Optimization Strategies**\n\n**Processing Efficiency**\n- **Batch Processing**: Group similar invoices for more efficient processing\n- **Template Recognition**: Create vendor-specific extraction templates\n- **Quality Scoring**: Implement confidence ratings for extracted data\n- **Error Handling**: Add fallback processes for failed extractions\n\n**Data Quality Assurance**\n- **Validation Rules**: Implement business logic for data verification\n- **Duplicate Detection**: Prevent duplicate invoice processing\n- **Manual Review Queues**: Flag uncertain extractions for human review\n- **Audit Logging**: Maintain detailed processing logs for troubleshooting\n\n**Business Process Integration**\n- **Approval Workflows**: Add management approval steps for high-value invoices\n- **Exception Handling**: Create special processes for unusual invoice types\n- **Reporting Automation**: Generate regular summaries of processed invoices\n- **Archive Management**: Implement retention policies for processed documents\n\n## 📈 Success Metrics\n\n### **Efficiency Improvements**\n- **Processing Time**: Reduce manual data entry from hours to minutes\n- **Accuracy Rates**: Achieve 95%+ data extraction accuracy\n- **Volume Capacity**: Process 10-50x more invoices with same resources\n- **Error Reduction**: Eliminate manual transcription errors\n\n### **Business Impact Measurements**\n- **Cost Savings**: Calculate labor cost reduction from automation\n- **Cash Flow Management**: Faster invoice processing enables better payment scheduling\n- **Compliance**: Improved audit trails and data consistency\n- **Scalability**: Ability to handle business growth without proportional staff increase\n\n## 📞 Questions & Support\n\nNeed help implementing or optimizing your AI Invoice Processor Agent?\n\n**📧 Expert Technical Support**\n- **Email**: Yaron@nofluff.online\n- **Response Time**: Within 24 hours on business days\n- **Specialization**: Invoice processing automation, AI data extraction, accounting workflow integration\n\n**🎥 Comprehensive Training Resources**\n- **YouTube Channel**: [https://www.youtube.com/@YaronBeen/videos](https://www.youtube.com/@YaronBeen/videos)\n - Complete setup and configuration walkthroughs\n - Advanced customization for different invoice types\n - Integration tutorials for popular accounting software\n - Troubleshooting common extraction and processing issues\n - Best practices for financial document automation\n\n**🤝 Professional Community & Updates**\n- **LinkedIn**: [https://www.linkedin.com/in/yaronbeen/](https://www.linkedin.com/in/yaronbeen/)\n - Connect for ongoing automation consulting and support\n - Share your invoice processing success stories and ROI results\n - Access exclusive workflow templates and advanced configurations\n - Join discussions about financial automation trends and innovations\n\n**💬 Support Request Guidelines**\nInclude in your support message:\n- Your current invoice processing volume and types\n- Specific vendor formats or invoice layouts you handle\n- Target accounting software or systems for integration\n- Any technical errors or extraction accuracy issues\n- Current manual processing workflow and pain points\n\n\n\n---\n\n*Ready to eliminate manual invoice processing forever? Deploy this AI Invoice Processor Agent and transform your accounting workflow from tedious data entry into intelligent, automated financial management!*", - "nodes": [ - "n8n-nodes-base.gmailTrigger", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.googleDrive", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.filter", - "n8n-nodes-base.extractFromFile", - "n8n-nodes-base.code", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:gmail", "ai", "integration:googleSheets"], - "triggerType": "gmail", - "hasAI": true, - "score": 86.41, - "scoreBreakdown": { - "traction": 0.598, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.889 - }, - "source": "https://n8n.io/workflows/4376", - "author": "yaron-nofluff", - "success": true - }, - { - "id": 4247, - "slug": "invoice-processor-validator-with-ocr-ai-google-sheets-4247", - "name": "Invoice Processor & Validator with OCR, AI & Google Sheets", - "description": "### 📝 Say goodbye to manual invoice checking! \n**This smart workflow automates** your entire invoice processing pipeline using **AI**, **OCR**, and **Google Sheets**.\n\n---\n\n### ⚙️ **What This Workflow Does:**\n\n📥 **1. Reads an invoice PDF**\n— Select a local PDF invoice from your machine.\n\n🔍 **2. Extracts raw text using OCR**\n— Converts scanned or digital PDFs into readable text.\n\n🧠 **3. AI Agent processes the text**\n— Transforms messy raw text into clean JSON using natural language understanding.\n\n🧱 **4. Structures and refines the JSON**\n— Converts AI output into a structured, usable format.\n\n🔄 **5. Splits item-wise data**\n— Extracts individual invoice line items with all details.\n\n🆔 **6. Generates unique keys**\n— Creates a unique identifier for each item for tracking.\n\n📊 **7. Updates Google Sheet**\n— Adds extracted items to your designated sheet automatically.\n\n📂 **8. Fetches master item data**\n— Loads your internal product master to validate against.\n\n✅ **9. Validates item name & cost**\n— Compares extracted items with your official records to verify accuracy.\n\n📌 **10. Updates results per item**\n— Marks each item as **Valid** or **Invalid** in the sheet based on matching.\n\n---\n\n### 💼 **Use Case:**\n\nPerfect for businesses, freelancers, or operations teams who receive invoices and want to **automate validation**, **detect billing errors**, and **log everything seamlessly in Google Sheets** — all using the power of **AI + n8n**.\n\n> 🔁 Fast. Accurate. Zero manual work.\n\n---\n`#OCR #AI #Invoices #Automation`.\n", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "@n8n/n8n-nodes-langchain.lmChatOpenRouter", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.code", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.readWriteFile", - "n8n-nodes-base.extractFromFile", - "n8n-nodes-base.if", - "n8n-nodes-base.set", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:manual", "ai", "integration:googleSheets"], - "triggerType": "manual", - "hasAI": true, - "score": 86.33, - "scoreBreakdown": { - "traction": 0.638, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.714 - }, - "source": "https://n8n.io/workflows/4247", - "author": "itechdp", - "success": true - }, - { - "id": 6067, - "slug": "auto-generate-seo-blog-posts-with-perplexity-gpt-leonardo-wo-6067", - "name": "Auto-Generate SEO Blog Posts with Perplexity, GPT, Leonardo & WordPress", - "description": "## ✨ SEO Blog Post Automation with Perplexity, GPT, Leonardo AI & WordPress\n\nThis workflow automates the creation and publishing of weekly SEO-optimized blog posts using AI and publishes them directly to WordPress — with featured images and tracking in Google Sheets.\n\n---\n\n## 🧠 Who is this for\n\nThis automation is ideal for:\n\n- Startup platforms and tech blogs \n- Content creators and marketers \n- Solopreneurs who want consistent blog output \n- Spanish-speaking audiences focused on startup trends \n\n---\n\n## ⚙️ What it does\n\n- ⏰ Runs every Monday at 6:00 AM via CRON \n- 📡 Uses Perplexity AI to research trending startup topics \n- 📝 Generates a 1000–1500 word article with GPT in structured HTML \n- 🎨 Creates a cinematic blog image using Leonardo AI \n- 🖼️ Uploads the image to WordPress with alt text and SEO-friendly filename \n- 📰 Publishes the post in a pre-defined category \n- 📊 Logs the post in Google Sheets for tracking \n\n---\n\n## 🚀 How to set it up\n\n1. **Connect your credentials**:\n - Perplexity API \n - OpenAI (GPT-4.1 Mini or similar) \n - Leonardo AI (Bearer token) \n - WordPress (Basic Auth) \n - Google Sheets (OAuth2)\n\n2. **Customize your content**:\n - Adjust the prompt inside the HTTP node to fit your tone or focus \n - Change the WordPress category ID \n - Update scheduling if you want a different publishing day \n\n3. **Test the workflow manually** to ensure all steps function correctly\n\n---\n\n## 💡 Pro tips\n\n- Add Slack or email nodes to get notified when a post goes live \n- Use multiple categories or RSS feeds for content diversification \n- Adjust GPT prompt to support different languages or tones \n- Add post-validation rules if needed before publishing \n\n---\n\n## 🎯 Why this matters\n\nThis workflow gives you a full editorial process on autopilot: research, writing, design, publishing, and tracking — all powered by AI. No more blank pages or manual posting.\n\n**Use it to scale your content strategy, boost your SEO, and stay relevant — 100% hands-free.**\n", - "nodes": [ - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.httpRequest", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.if", - "n8n-nodes-base.wait", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.code", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:schedule", "ai", "integration:httpRequest"], - "triggerType": "schedule", - "hasAI": true, - "score": 86.33, - "scoreBreakdown": { - "traction": 0.682, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.538 - }, - "source": "https://n8n.io/workflows/6067", - "author": "cristiantala", - "success": true - }, - { - "id": 7163, - "slug": "automate-hyper-personalized-email-outreach-with-ai-gmail-goo-7163", - "name": "Automate hyper-personalized email outreach with AI, Gmail & Google Sheets", - "description": "# This n8n template from [Intuz](https://www.intuz.com/) provides a complete and automated solution for hyper-personalized email outreach. \n\nIt powerfully combines AI with Gmail and Google Sheets, using specific keywords and prospect data to automatically craft unique, compelling email content that boosts engagement and secures more replies. \n\nInstead of manually replying to every lead or inquiry, this template does the heavy lifting for you, ensuring every response is relevant, thoughtful, and timely.\n\nIt reads each person's unique inquiry, uses OpenAI to craft a perfectly tailored and human-like response, and sends it directly from your Gmail account. Ideal for sales, marketing, and customer support teams looking to boost engagement and save hours of manual work.\n\n## Use Cases:\n- Sales Teams: Instantly follow up with new leads from your website's contact form with a personalized touch.\n\n- Customer Support: Provide initial, intelligent responses to support tickets, answering common questions or acknowledging receipt of a complex issue.\n\n- Marketing Automation: Nurture leads by responding to content downloads or webinar sign-ups with relevant, non-generic information.\n\n- Founders & Solopreneurs: Manage all incoming business inquiries (partnerships, media, etc.) efficiently without sacrificing quality.\n\n\n## How It Works:\n1. Trigger the Flow (Manual): Start the automation whenever you're ready to process a new batch of inquiries from your sheet.\n\n2. Fetch Inquiries from Google Sheets: The workflow connects to your specified Google Sheet and reads each row. It pulls the contact's First Name, Email ID, the Inquiry Intent (e.g., \"Demo Request,\" \"Pricing Inquiry\"), and the full text of their Original Inquiry.\n\n3. Sync Your Signature: Before writing the email, an HTTP Request node dynamically fetches your display name from your Gmail account settings. This ensures the signature in the generated email (Thanks, {{Your Name}}) is always accurate.\n\n4. Craft a Hyper-Personalized Reply with AI: It uses this context to generate a high-quality, professional, and friendly email reply in HTML format. For example:\n- If the intent is \"Technical Support,\" the AI will generate a helpful, empathetic response addressing the technical issue.\n- If the intent is \"Partnership Proposal,\" it will draft a professional reply acknowledging the proposal and outlining the next steps.\n\n5. Send via Gmail: The final node takes the AI-generated message, adds a relevant subject line (e.g., \"Re: Your Demo Request\"), and sends it directly to the contact's email address from your connected Gmail account.\n\nThis process loops for every single row in your Google Sheet, turning a list of names into a series of meaningful conversations.\n\n## Setup Instructions:\nTo get this workflow running, you'll need to configure a few things:\n\n1. Credentials:\nGoogle: Connect your Google account via OAuth2 and ensure you have enabled access for Google Sheets, Google Drive, and Gmail.\n2. OpenAI: Add your OpenAI API key as a credential.\n3. Google Sheet Setup:\nCreate a Google Sheet with the following exact column headers:\n-First Name\n-Email ID\n-Inquiry Intent (A short category like \"Demo Request\", \"Billing Issue\", etc.)\n-Original Inquiry (The full text of the email or message you received).\n4. Node Configuration:\nGet row(s) in sheet: Select your Google Sheet document and the specific sheet name.\nMessage a model (OpenAI): Choose your preferred OpenAI model (e.g., gpt-4-turbo, gpt-3.5-turbo).\n5. HTTP Request & Send Personalized emails: These nodes should automatically use your configured Gmail credentials. No changes are typically needed.\n\n\n## Connect with us\n\n- Website: https://www.intuz.com/n8n-workflow-automation-templates\n- Email: getstarted@intuz.com\n- LinkedIn: https://www.linkedin.com/company/intuz\n- Get Started: https://n8n.partnerlinks.io/intuz\n\n\n## For Custom Worflow Automation \n\n\nClick here- [Get Started](https://www.intuz.com/get-started)", - "nodes": [ - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.gmail", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.httpRequest" - ], - "tags": ["trigger:manual", "ai", "integration:googleSheets"], - "triggerType": "manual", - "hasAI": true, - "score": 86.3, - "scoreBreakdown": { - "traction": 0.565, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/7163", - "author": "intuz", - "success": true - }, - { - "id": 3435, - "slug": "get-qualified-leads-in-one-click-from-apollo-to-airtable-3435", - "name": "Get Qualified Leads in One Click from Apollo to Airtable", - "description": "## You Don’t Need More Tools. You Just Need the Right Leads.\nWhy spend $1,000s on lead gen when your perfect leads are already waiting in Apollo? You’ve already filtered the ideal prospects. You know who they are, where they work, and what they do.\n\nNow **imagine turning that list into enriched, ready-to-contact leads—without paying pricey Apollo's recurring subscription** *(spoiler: you will pay only 0.60$ per 500 leads)*.\n\n## From Filter to Outreach-Ready in Seconds\nWith the Lead Generation System, you just drop your Apollo search URL.\n\n**The workflow does the rest:**\n✅ Scrapes all matching contacts from your Apollo filter\n✅ Enriches and organizes the data (names, roles, emails, LinkedIns, companies, etc.)\n✅ Delivers the final lead list to Airtable—or your CRM of choice\n\nNo more manual exports. \nNo CSV mess. \nNo VA needed.\nJust qualified leads, cleaned and ready to go.\n\n## Perfect For\n- Founders doing DIY outbound\n- Growth marketers scaling cold email\n- Agencies running lead-gen for clients\n- Anyone tired of paying too much for messy, outdated lists\n\n\n## Setup Guide\nI built a step-by-step guide to setup this workflow in 5 to 10 minutes, available here: [https://notanothermarketer.gitbook.io/home/templates/lead-generation](https://notanothermarketer.gitbook.io/home/templates/lead-generation)\n\nThis template is free. Enjoy!\n", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.set", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.airtable", - "n8n-nodes-base.if" - ], - "tags": ["trigger:manual", "integration:set"], - "triggerType": "manual", - "hasAI": false, - "score": 86.15, - "scoreBreakdown": { - "traction": 0.558, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/3435", - "author": "notanothermarketer", - "success": true - }, - { - "id": 4359, - "slug": "ai-powered-post-sales-call-automated-proposal-generator-4359", - "name": "AI-Powered Post-Sales Call Automated Proposal Generator", - "description": "# AI-Powered Proposal Generator - Sales Automation Workflow\n\n## Overview\n![Screenshot 20250524 143046.png](fileId:1373)\nThis n8n workflow automates the entire proposal generation process using AI, transforming client requirements into professional, customized proposals delivered via email in seconds.\n\n## Use Case\nPerfect for agencies, consultants, and sales teams who need to generate high-quality proposals quickly. Instead of spending hours writing proposals manually, this workflow captures client information through a web form and uses GPT-4 to generate contextually relevant, professional proposals.\n\n## How It Works\n1. **Form Trigger** - Captures client information through a customizable web form\n2. **OpenAI Integration** - Processes form data and generates structured proposal content\n3. **Google Drive** - Creates a copy of your proposal template\n4. **Google Slides** - Populates the template with AI-generated content\n5. **Gmail** - Automatically sends the completed proposal to the client\n\n## Key Features\n- **AI Content Generation**: Uses GPT-4 to create personalized proposal content\n- **Professional Templates**: Integrates with Google Slides for polished presentations\n- **Automated Delivery**: Sends proposals directly to clients via email\n- **Form Integration**: Captures all necessary client data through web forms\n- **Customizable Output**: Generates structured proposals with multiple sections\n\n## Template Sections Generated\n- Proposal title and description\n- Problem summary analysis\n- Three-part solution breakdown\n- Project scope details\n- Milestone timeline with dates\n- Cost integration\n\n## Requirements\n- **n8n instance** (cloud or self-hosted)\n- **OpenAI API key** for content generation\n- **Google Workspace account** for Slides and Gmail\n- **Basic n8n knowledge** for setup and customization\n\n## Setup Complexity\n**Intermediate** - Requires API credentials setup and basic workflow customization\n\n## Benefits\n- **Time Savings**: Reduces proposal creation from hours to minutes\n- **Consistency**: Ensures all proposals follow the same professional structure\n- **Personalization**: AI analyzes client needs for relevant content\n- **Automation**: Eliminates manual copy-paste and formatting work\n- **Scalability**: Handle multiple proposal requests simultaneously\n\n## Customization Options\n- Modify AI prompts for different industries or services\n- Customize Google Slides template design\n- Adjust form fields for specific information needs\n- Personalize email templates and signatures\n- Configure milestone templates for different project types\n\n## Error Handling\nIncludes basic error handling for API failures and form validation to ensure reliable operation.\n\n## Security Notes\nAll credentials have been removed from this template. Users must configure their own:\n- OpenAI API credentials\n- Google OAuth2 connections for Slides, Drive, and Gmail\n- Form webhook configuration\n\nThis workflow demonstrates practical AI integration in business processes and showcases n8n's capabilities for complex automation scenarios.", - "nodes": [ - "n8n-nodes-base.googleSlides", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.googleDrive", - "n8n-nodes-base.gmail", - "n8n-nodes-base.formTrigger", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:formTrigger", "ai", "integration:googleSlides"], - "triggerType": "formTrigger", - "hasAI": true, - "score": 86.1, - "scoreBreakdown": { - "traction": 0.555, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/4359", - "author": "dominixai", - "success": true - }, - { - "id": 14216, - "slug": "generate-concert-ticket-pdfs-with-qr-codes-using-pdf-generat-14216", - "name": "Generate concert ticket PDFs with QR codes using PDF Generator API", - "description": "Generate personalized concert ticket PDFs with QR codes using PDF Generator API, then email them to attendees, log sales to Google Sheets, and notify organizers via Slack — all triggered from a simple web form.\n\n## Who is this for\n\nEvent organizers, ticketing teams, and developers who need an automated pipeline to issue branded PDF concert tickets with unique QR codes for venue entry — without building a custom backend.\n\n## How it works\n\n1. An attendee fills out a **web form** with their name, email, event details, seat number, and ticket tier (General / VIP / Backstage).\n2. The workflow generates a unique ticket ID and prepares all data for the PDF template.\n3. **PDF Generator API** renders a personalized PDF ticket. The QR code is a native template component that encodes the ticket ID automatically.\n4. A styled **HTML confirmation email** with a download link is sent to the attendee via Gmail.\n5. The ticket details are logged to a **Google Sheets** spreadsheet for tracking and attendance management.\n6. A **Slack notification** alerts the event organizer with a summary of the newly issued ticket.\n\n## Set up\n\n1. **PDF Generator API** — Sign up at [pdfgeneratorapi.com](https://pdfgeneratorapi.com), create a ticket template with a QR Code component bound to `{{ ticket_id }}`, and note your template ID.\n2. **Template ID** — Open the \"Prepare Ticket Data\" Code node and replace the `TEMPLATE_ID` value with your own.\n3. **Credentials** — Connect your accounts in each node: PDF Generator API, Gmail, Google Sheets, and Slack.\n4. **Google Sheets** — Create a spreadsheet with columns: `Ticket ID`, `Attendee`, `Email`, `Event`, `Venue`, `Date`, `Seat`, `Tier`, `PDF URL`, `Issued At`. Set the spreadsheet ID in the \"Log Ticket Sale\" node.\n5. **Slack** — Choose a channel (e.g. `#tickets`) in the \"Notify Event Organizer\" node.\n\n## Requirements\n\n- [PDF Generator API](https://pdfgeneratorapi.com) account (free trial available)\n- Gmail account (OAuth)\n- Google Sheets account (OAuth)\n- Slack workspace (optional — remove the last node if not needed)\n\n## How to customize\n\n- **Output format** — The PDF node returns a hosted URL by default (valid 30 days). Switch to `File` output to attach the PDF directly to the email instead.\n- **Ticket tiers** — Add or rename tiers in the form node and update the tier mapping logic in the \"Prepare Ticket Data\" Code node.\n- **Email design** — Edit the \"Build Confirmation Email\" Code node to match your brand colors and layout.\n- **Remove Slack** — Simply delete the \"Notify Event Organizer\" node if you don't need organizer alerts.\n- **Add payment** — Insert a Stripe or payment node before the form confirmation to handle paid tickets.", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.formTrigger", - "n8n-nodes-base.code", - "@pdfgeneratorapi/n8n-nodes-pdf-generator-api.pdfGeneratorApi", - "n8n-nodes-base.gmail", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.slack" - ], - "tags": ["trigger:formTrigger", "integration:code"], - "triggerType": "formTrigger", - "hasAI": false, - "score": 86.1, - "scoreBreakdown": { - "traction": 0.591, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.857 - }, - "source": "https://n8n.io/workflows/14216", - "author": "marian", - "success": true - }, - { - "id": 3705, - "slug": "generate-custom-ai-images-with-openai-gpt-image-1-model-3705", - "name": "Generate Custom AI Images with OpenAI GPT-Image-1 Model", - "description": "**How it works** \n1. Trigger the workflow manually via the n8n UI. \n2. Define key parameters like the image prompt, number of images, size, quality, and model. \n3. Send a POST request to OpenAI’s image generation API using those inputs. \n4. Split the API response to handle multiple images. \n5. Convert the base64 image data into downloadable binary files.\n\n**Set up steps** \nInitial setup takes around 5–10 minutes. You’ll need an OpenAI API key, a configured HTTP Request node with credentials, and to customize the prompt/parameter fields in the “Set Variables” node. No advanced config or external services needed.\n\n**Important Note**\nYou have to make sure to complete OpenAI's new verification requirements to use their new image API:\nhttps://help.openai.com/en/articles/10910291-api-organization-verification\n\nIt only takes a few minutes and does not cost any money.", - "nodes": [ - "n8n-nodes-base.convertToFile", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.set", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:manual", "integration:convertToFile"], - "triggerType": "manual", - "hasAI": false, - "score": 86.08, - "scoreBreakdown": { - "traction": 0.554, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/3705", - "author": "aiwithapex", - "success": true - }, - { - "id": 3942, - "slug": "ai-powered-restaurant-order-chatbot-with-gpt-4o-for-pos-inte-3942", - "name": "AI-Powered Restaurant Order Chatbot with GPT-4o for POS Integration", - "description": "This workflow automates the restaurant POS (Point of Sale) data management process, facilitating seamless order handling, customer tracking, inventory management, and sales reporting. It retrieves order details, processes payment information, updates inventory, and generates real-time sales reports, all integrated into a centralized system that improves restaurant operations.\n\nThe workflow integrates various systems, including a POS terminal to gather order data, payment gateways to process transactions, inventory management tools to update stock, and reporting tools like Google Sheets or an internal database for generating sales and performance reports.\n\nWho Needs Restaurant POS Automation?\nThis POS automation workflow is ideal for restaurant owners, managers, and staff looking to streamline their operations:\n\nRestaurant Owners – Automate order processing, track sales, and monitor inventory to ensure smooth operations.\n\nManagers – Access real-time sales data and performance reports to make informed decisions.\n\nStaff – Reduce manual work, focusing on providing better customer service while the system handles orders and payments.\n\nInventory Teams – Automatically update inventory levels based on orders and ingredient usage.\n\nIf you need a reliable and automated POS solution to manage restaurant orders, payments, inventory, and reporting, this workflow minimizes human error, boosts efficiency, and saves valuable time.\n\nWhy Use This Workflow?\n\nEnd-to-End Automation – Automates everything from order input to inventory updates and sales reporting.\n\nSeamless Integration – Connects POS, payment systems, inventory management, and reporting tools for smooth data flow.(if needed)\n\nReal-Time Data – Provides up-to-the-minute reports on sales, stock levels, and order statuses.\n\nScalable & Efficient – Supports multiple locations, multiple users, and high order volumes.\n\nStep-by-Step: How This Workflow Manages POS Data\n\nCollect Orders – Retrieves order details from the POS system, including customer information, ordered items, and payment details.\n\nUpdate Inventory – Decreases inventory levels based on sold items, ensuring stock counts are always accurate.\n\nGenerate Reports – Compiles sales, revenue, and inventory data into real-time reports and stores them in Google Sheets or an internal database.\n\nTrack Customer Data – Keeps a log of customer details and order history for better service and marketing insights.\n\nCustomization: Tailor to Your Needs\n\nMultiple POS Systems – Adapt the workflow to work with different POS systems or terminals based on your restaurant setup.\n\nCustom Reporting – Modify the reporting format or include specific sales metrics (e.g., daily totals, best-selling items, employee performance).\n\nInventory Management – Adjust inventory updates to include alerts when stock reaches critical levels or needs reordering.\n\nIntegration with Accounting Software – Connect with platforms like QuickBooks for automated financial tracking.\n\n🔑 Prerequisites\n\nPOS System Integration – Ensure the POS system can export order data in a compatible format.\n\nPayment Gateway API – Set up the necessary API keys for payment processing (e.g., Stripe, PayPal).\n\nInventory Management Tools – Use inventory software or databases that can automatically update stock levels.\n\nReporting Tools – Use Google Sheets or an internal database to store and generate sales and inventory reports.\n\n🚀 Installation & Setup\n\nConfigure Credentials\n\nSet up API credentials for payment gateways and inventory management tools.\n\nImport Workflow\n\nImport the workflow into your automation platform (e.g., n8n, Zapier).\n\nLink POS system, payment gateway, and inventory management systems.\n\nTest & Run\n\nProcess a test order to ensure that data flows correctly through each step.\n\nVerify that inventory updates and reports are generated as expected.\n\n⚠ Important\n\nData Privacy – Ensure compliance with data protection regulations (e.g., GDPR, PCI DSS) when handling customer payment and order data.\n\nSystem Downtime – Monitor system performance to ensure that the workflow runs without disruptions during peak hours.\n\nSummary\nThis restaurant POS automation workflow integrates order management, payment processing, inventory updates, and real-time reporting, enabling efficient restaurant operations. Whether you are running a single location or a chain of restaurants, this solution streamlines daily tasks, reduces errors, and provides valuable insights, saving time and improving customer satisfaction. 🚀", - "nodes": [ - "@n8n/n8n-nodes-langchain.informationExtractor", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.if", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.code", - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.noOp", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.executeWorkflowTrigger", - "@n8n/n8n-nodes-langchain.chatTrigger", - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.toolWorkflow", - "@n8n/n8n-nodes-langchain.memoryBufferWindow" - ], - "tags": ["trigger:other", "ai", "integration:langchain"], - "triggerType": "other", - "hasAI": true, - "score": 86.07, - "scoreBreakdown": { - "traction": 0.589, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.857 - }, - "source": "https://n8n.io/workflows/3942", - "author": "shivam840708", - "success": true - }, - { - "id": 3912, - "slug": "automate-lead-qualification-with-retellai-phone-agent-openai-3912", - "name": "Automate Lead Qualification with RetellAI Phone Agent, OpenAI GPT & Google Sheet", - "description": "![Workflow Screenshot](https://www.dr-firas.com/Build-a-Phone-Agent.png)\n\n## 👉 Build a Phone Agent to qualify outbound leads and schedule inbound calls\n\n\n### Who is this for?\n\nThis workflow is designed for **sales teams**, **call centers**, and **businesses handling both outbound and inbound lead calls** who want to automate their qualification, follow-up, and call documentation process without manual intervention. It’s ideal for teams using **Google Sheets, RetellAI, OpenAI, and Gmail** as part of their tech stack.\n\n---\n\n### Real-World Use Cases\n\n- 🛍 E-commerce – Instantly handle product FAQs and order status checks, 24/7.\n- 🏬 Retail Stores – Share store hours, directions, and return policies without lifting a finger.\n- 🍽 Restaurants – Take reservations or answer menu questions automatically.\n- 💼 Service Providers – Book appointments or consultations while you focus on your craft.\n- 📞 Any Local Business – Deliver friendly, consistent phone support — no live agent required.\n\n---\n\n### What problem is this workflow solving?\n\nManaging lead calls at scale can be chaotic—between scheduling outbound qualification calls, handling inbound appointment requests, and making sure every call is documented and followed up. This workflow automates the entire process, reducing human error and saving time by:\n\n- ✅ Sending reminders to reps for outbound calls\n- ✅ Automatically placing calls with RetellAI\n- ✅ Handling inbound calls and checking caller details\n- ✅ Generating and emailing call summaries automatically\n\n---\n\n### What this workflow does\n\nThis n8n template connects Google Sheets, RetellAI, OpenAI, and Gmail into a seamless workflow:\n\n1. **Outbound Lead Qualification Workflow**\n - Triggers when a new lead is added to Google Sheets\n - Sends an SMS notification to remind the rep to call in 5 minutes\n - (Optional) Waits 5 minutes\n - Initiates an automated call to the lead via RetellAI\n\n2. **Inbound Call Appointment Scheduler**\n - Receives inbound calls from RetellAI (via webhook)\n - Checks if the caller’s number exists in Google Sheets\n - Responds to RetellAI with a success or error message\n\n3. **Post-Call Workflow**\n - Receives post-call data from RetellAI\n - Filters only analyzed calls\n - Updates the lead’s record in Google Sheets\n - Uses OpenAI to generate a call summary\n - Emails the summary to a team inbox or rep\n\n---\n\n### Setup\n\n✅ You need an active **RetellAI API key**\n\n1. Sign up for RetellAI, create an agent, and set the webhook URLs (n8n_call for call events).\n2. Purchase a Twilio phone number and link it to the agent. \n\n✅ Your **Google Sheet must have a column for phone numbers** (e.g., \"Phone\") \n✅ **Gmail account** connected and authorized in n8n \n✅ **OpenAI API key** added to your environment variables or credentials\n\n1. Configure your Google Sheets node with the correct spreadsheet ID and range\n2. Add your RetellAI API key to the HTTP request nodes\n3. Connect your Gmail account in the Gmail node\n4. Add your OpenAI key in the OpenAI node\n\n👉 See full setup guide here: [Notion Documentation](https://automatisation.notion.site/Build-a-Phone-Agent-to-qualify-outbound-leads-and-schedule-inbound-calls-1eb3d6550fd9807993dce3c6ed111554)\n\n---\n\n### How to customize this workflow to your needs\n\n- **Change SMS content**: Edit the text in the “Send SMS reminder” node to match your team’s tone\n- **Modify call wait time**: Enable and adjust the “Wait 5 minutes” node to any delay you prefer\n- **Add CRM integration**: Replace or extend the Google Sheets node to update your CRM instead of a spreadsheet\n- **Customize call summary prompts**: Edit the prompt sent to OpenAI to change the summary style or add extra insights\n- **Send email to different recipients**: Change the recipient address in the Gmail node or make it dynamic from the lead record\n\n\n---\n\n### Need help customizing?\nContact me for consulting and support : [Linkedin](https://www.linkedin.com/in/dr-firas/)", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.googleSheetsTrigger", - "n8n-nodes-base.twilio", - "n8n-nodes-base.wait", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.webhook", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.respondToWebhook", - "n8n-nodes-base.filter", - "n8n-nodes-base.if", - "n8n-nodes-base.gmail", - "@n8n/n8n-nodes-langchain.openAi" - ], - "tags": ["trigger:other", "ai", "integration:googleSheets"], - "triggerType": "other", - "hasAI": true, - "score": 86, - "scoreBreakdown": { - "traction": 0.603, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.786 - }, - "source": "https://n8n.io/workflows/3912", - "author": "drfiras", - "success": true - }, - { - "id": 3900, - "slug": "automated-youtube-video-scheduling-ai-metadata-generation-3900", - "name": "Automated YouTube Video Scheduling & AI Metadata Generation 🎬", - "description": "\n\n## 👥 Who Is This For?\nContent creators, marketing teams, and channel managers who need to streamline video publishing with optimized metadata and scheduled releases across multiple videos.\n\n## 🛠 What Problem Does This Solve?\nManual YouTube video publishing is time-consuming and often results in inconsistent descriptions, tags, and scheduling. This workflow fully automates:\n* Extracting video transcripts via Apify for metadata generation\n* Creating SEO-optimized descriptions and tags for each video\n* Setting videos to private during initial upload (critical for scheduling)\n* Implementing scheduled publishing at strategic times\n* Maintaining consistent branding and formatting across all content\n\n## 🔄 Node-by-Node Breakdown\n| Step | Node Purpose |\n|------|--------------|\n| 1 | Every Day (Scheduler) | Trigger workflow on a regular schedule |\n| 2 | Get Videos to Harmonize | Retrieve videos requiring metadata enhancement |\n| 3 | Get Video IDs (Unpublished) | Filter for videos that need publishing |\n| 4 | Loop over Video IDs | Process each video individually |\n| 5 | Get Video Data | Retrieve metadata for the current video |\n| 6 | Loop over Videos with Parameter IS | Set parameters for processing |\n| 7 | Set Videos to Private | Ensure videos are private (required for scheduling) |\n| 8 | Apify: Get Transcript | Extract video transcript via Apify |\n| 9 | Fetch Latest Videos | Get most recent channel content |\n| 10 | Loop Over Items | Process each video item |\n| 11 | Generate Description, Tags, etc. | Create optimized metadata from transcript |\n| 12 | AP Clean ID | Format identifiers |\n| 13 | Retrieve Generated Data | Collect the enhanced metadata |\n| 14 | Adjust Transcript Format | Format transcript for better processing |\n| 15 | Update Video's Metadata | Apply generated description and tags to video |\n\n## ⚙️ Pre-conditions / Requirements\n* n8n with YouTube API credentials configured\n* Apify account with API access for transcript extraction\n* YouTube channel with upload permissions\n* Master templates for description formatting\n* Videos must be initially set to private for scheduling to work\n\n## ⚙️ Setup Instructions\n1. Import this workflow into your n8n instance.\n2. Configure YouTube API credentials with proper channel access.\n3. Set up Apify integration with appropriate actor for transcript extraction.\n4. Define scheduling parameters in the Every Day node.\n5. Configure description templates with placeholders for dynamic content.\n6. Set default tags and customize tag generation rules.\n7. Test with a single video before batch processing.\n\n## 🎨 How to Customize\n* Adjust prompt templates for description generation to match your brand voice.\n* Modify tag selection algorithms based on your channel's SEO strategy.\n* Create multiple publishing schedules for different content categories.\n* Integrate with analytics tools to optimize publishing times.\n* Add notification nodes to alert when videos are successfully scheduled.\n\n## ⚠️ Important Notes\n* Videos MUST be uploaded as private initially - the Publish At logic only works for private videos that haven't been published before.\n* Publishing schedules require videos to remain private until their scheduled time.\n* Transcript quality affects metadata generation results.\n* Consider YouTube API quotas when scheduling large batches of videos.\n\n## 🔐 Security and Privacy\n* API credentials are stored securely within n8n.\n* Transcripts are processed temporarily and not stored permanently.\n* Webhook URLs should be protected to prevent unauthorized triggering.\n* Access to the workflow should be limited to authorized team members only.", - "nodes": [ - "n8n-nodes-base.youTube", - "n8n-nodes-base.splitInBatches", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.wait", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.code", - "n8n-nodes-base.set", - "n8n-nodes-base.removeDuplicates", - "n8n-nodes-base.if", - "n8n-nodes-base.httpRequest" - ], - "tags": ["trigger:manual", "ai", "integration:youTube"], - "triggerType": "manual", - "hasAI": true, - "score": 85.9, - "scoreBreakdown": { - "traction": 0.665, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.52 - }, - "source": "https://n8n.io/workflows/3900", - "author": "stardawnai", - "success": true - }, - { - "id": 3580, - "slug": "scrape-linkedin-job-listings-for-hiring-signals-prospecting--3580", - "name": "Scrape LinkedIn Job Listings for Hiring Signals & Prospecting with Bright Data", - "description": "LinkedIn Hiring Signal Scraper — Jobs & Prospecting Using Bright Data\n\n## Purpose:\nDiscover recent job posts from LinkedIn using Bright Data's Dataset API, clean the results, and log them into Google Sheets — for both job hunting **and** identifying high-intent B2B leads based on hiring activity.\n\n## Use Cases:\n- **Job Seekers** – Spot relevant openings filtered by role, city, and country. \n- **Sales & Prospecting** – Use job posts as buying signals. \n If a company is hiring for a role you support (e.g. marketers, developers, ops) — \n it's the perfect time to reach out and offer your services.\n\n## Tools Needed:\n- **n8n Nodes:**\n - Form Trigger \n - HTTP Request \n - Wait \n - If \n - Code \n - Google Sheets \n - Sticky Notes (for embedded guidance)\n- **External Services:**\n - [Bright Data](https://brightdata.com) (Dataset API) \n - [Google Sheets](https://docs.google.com/spreadsheets/d/1_jbr5zBllTy_pGbogfGSvyv1_0a77I8tU-Ai7BjTAw4/edit?usp=sharing)\n\n## API Keys & Authentication Required:\n- **Bright Data API Key** \n → Add in the HTTP Request headers: \n `Authorization: Bearer YOUR_BRIGHTDATA_API_KEY`\n\n- **Google Sheets OAuth2** \n → Connect your account in n8n to allow read/write access to the spreadsheet.\n\n## General Guidelines:\n- Use descriptive names for all nodes. \n- Include retry logic in polling to avoid infinite loops. \n- Flatten nested fields (like `job_poster` and `base_salary`). \n- Strip out HTML tags from job descriptions for clean output.\n\n## Things to be Aware Of:\n- Bright Data snapshots take ~1–3 minutes — use a Wait node and polling. \n- Form filters affect output significantly: \n 🔍 We recommend filtering by **\"Last 7 days\"** or **\"Past 24 hours\"** for fresher data. \n- Avoid hardcoding values in the form — leave optional filters empty if unsure.\n\n## Post-Processing & Outreach:\n- After data lands in Google Sheets, you can use it to: \n - Personalize cold emails based on job titles, locations, and hiring signals. \n - Send thoughtful LinkedIn messages (e.g., \"Saw you're hiring a CMO...\") \n - Prioritize outreach to companies actively growing in your niche.\n\n## Additional Notes:\n- 📄 Copy the Google Sheet Template: \n [Click here to make your copy](https://docs.google.com/spreadsheets/d/1_jbr5zBllTy_pGbogfGSvyv1_0a77I8tU-Ai7BjTAw4/edit?usp=sharing) \n → Rename for each campaign or client.\n\n- Form fields include: \n - Job Location (city or region) \n - Keyword (e.g., CMO, Backend Developer) \n - Country (2-letter code, e.g., US, UK)\n\n---\n\nThis workflow gives you a competitive edge — \n📌 For candidates: Be first to apply. \n📌 For sellers: Be first to pitch. \nAll based on live hiring signals from LinkedIn.\n\n---\n\n## STEP-BY-STEP WALKTHROUGH\n\n### Step 1: Set up your Google Sheet\n1. Open this [template](https://docs.google.com/spreadsheets/d/1_jbr5zBllTy_pGbogfGSvyv1_0a77I8tU-Ai7BjTAw4/edit?usp=sharing)\n2. Go to `File → Make a copy`\n3. You'll use this copy as the destination for the scraped job posts\n\n### Step 2: Fill out the Input Form in n8n\nThe form allows you to define what kind of job posts you want to scrape.\n\n**Fields:**\n- **Job Location** → e.g. `New York`, `Berlin`, `Remote` \n- **Keyword** → e.g. `CMO`, `AI Architect`, `Ecommerce Manager` \n- **Country Code (2-letter)** → e.g. `US`, `UK`, `IL`\n\n**💡 Pro Tip:** \nFor best results, set the filter inside the workflow to: \n`time_range = \"Past 24 hours\"` or `\"Last 7 days\"` \nThis keeps results relevant and fresh.\n\n### Step 3: Trigger Bright Data Snapshot\nThe workflow sends a request to Bright Data with your input.\n\nExample API Call Body:\n```json\n[\n {\n \"location\": \"New York\",\n \"keyword\": \"Marketing Manager\",\n \"country\": \"US\",\n \"time_range\": \"Past 24 hours\",\n \"job_type\": \"Part-time\",\n \"experience_level\": \"\",\n \"remote\": \"\",\n \"company\": \"\"\n }\n]\n```\n\nBright Data will start preparing the dataset in the background.\n\n### Step 4: Wait for the Snapshot to Complete\nThe workflow includes a Wait Node and Polling Loop that checks every few minutes until the data is ready.\n\nYou don't need to do anything here — it's all automated.\n\n### Step 5: Clean Up the Results\nOnce Bright Data responds with the full job post list:\n\n✔️ Nested fields like job_poster and base_salary are flattened \n✔️ HTML in job descriptions is removed \n✔️ Final data is formatted for export\n\n### Step 6: Export to Google Sheets\nThe final cleaned list is added to your Google Sheet (first tab).\n\nEach row = one job post, with columns like:\n\njob_title, company_name, location, salary_min, apply_link, job_description_plain\n\n### Step 7: Use the Data for Outreach or Research\n#### Example for Job Seekers:\nYou search for:\n\n- Location: Berlin\n- Keyword: Product Designer\n- Country: DE\n- Time range: Past 7 days\n\nNow you've got a live list of roles — with salary, recruiter info, and apply links.\n→ Use it to apply faster than others.\n\n#### Example for Prospecting (Sales / SDR):\nYou search for:\n\n- Location: London\n- Keyword: Growth Marketing\n- Country: UK\n\nAnd find companies hiring growth marketers.\n→ That's your signal to offer help with media buying, SEO, CRO, or your relevant service.\n\nUse the data to:\n- Write personalized cold emails (\"Saw you're hiring a Growth Marketer…\")\n- Start warm LinkedIn outreach\n- Build lead lists of companies actively expanding in your niche\n\n## API Credentials Required:\n- **Bright Data API Key** \n Used in HTTP headers: `Authorization: Bearer YOUR_BRIGHTDATA_API_KEY`\n\n- **Google Sheets OAuth2** \n Allows n8n to read/write to your spreadsheet\n\n## Adjustments & Customization Tips:\n- Modify the HTTP Request body to add more filters (e.g. job_type, remote, company)\n- Increase or reduce polling wait time depending on Bright Data speed\n- Add scoring logic to prioritize listings based on title or location\n\n## Final Notes:\n- 📄 Google Sheet Template: \n [Make your copy here](https://docs.google.com/spreadsheets/d/1_jbr5zBllTy_pGbogfGSvyv1_0a77I8tU-Ai7BjTAw4/edit?usp=sharing)\n\n- ⚙️ Bright Data Dataset API: \n Visit [BrightData.com](https://get.brightdata.com/1tndi4600b25)\n\n- 📬 Personalization works best when you act quickly. \n Use the freshest data to reach out with context — not generic pitches.\n\n---\n\nThis workflow turns LinkedIn job posts into sales insights and job leads.\nAll in one click. Fully automated. Ready for your next move.", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.formTrigger", - "n8n-nodes-base.wait", - "n8n-nodes-base.if", - "n8n-nodes-base.code", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.set" - ], - "tags": ["trigger:formTrigger", "integration:httpRequest"], - "triggerType": "formTrigger", - "hasAI": false, - "score": 85.88, - "scoreBreakdown": { - "traction": 0.599, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.778 - }, - "source": "https://n8n.io/workflows/3580", - "author": "yaron-nofluff", - "success": true - }, - { - "id": 4057, - "slug": "auto-respond-to-gmail-enquiries-using-gpt-4o-dumpling-ai-lan-4057", - "name": "Auto-Respond to Gmail Enquiries using GPT-4o, Dumpling AI & LangChain Agent", - "description": "\n\n### Who is this for?\n\nThis workflow is perfect for customer support teams, sales departments, or solopreneurs who receive frequent email enquiries and want to automate the initial response process using AI. If you spend too much time answering similar questions, this system helps respond faster and more intelligently—without writing a single line of code.\n\n---\n\n### What problem is this workflow solving?\n\nManually responding to repeated customer enquiries slows productivity and increases delay. This workflow classifies if an incoming email is a real enquiry, analyzes the content with a LangChain-powered agent, fetches helpful context using Dumpling AI, and sends a personalized reply using Gmail—all within minutes.\n\n---\n\n### What this workflow does\n\n1. **Listens for new incoming Gmail messages** using the Gmail Trigger node. \n2. **Classifies whether the email is an enquiry** using a GPT-4o classification prompt. \n3. **Uses a Filter node** to continue only if the email was classified as an enquiry. \n4. **Passes the email content to a LangChain Agent**, enhanced with memory, AI tools, and Dumpling AI to search for relevant information. \n5. **The agent constructs a smart, relevant response**, then sends it to the original sender via Gmail.\n\n---\n\n### Setup\n\n1. **Connect Gmail** \n - Use the Gmail Trigger node to connect to the Gmail account that receives enquiries. \n - Make sure Gmail OAuth2 credentials are authenticated.\n\n2. **Configure Dumpling AI Agent** \n - Sign up at [Dumpling AI](https://www.dumplingai.com/). \n - Create an agent trained to search your help docs, site content, or FAQs. \n - Copy your Dumpling agent ID and API key. \n - Paste it in the `Dumpling AI Agent – Search for Relevant Info` HTTP Request node.\n\n3. **Set Up LangChain Agent** \n - No extra setup needed beyond connecting OpenAI credentials. \n - GPT-4o is used for classification and reply generation.\n\n4. **Enable Gmail Reply Node** \n - The final `Send Email Response via Gmail` node will send the AI-generated reply back to the same thread.\n\n---\n\n### How to customize this workflow to your needs\n\n- Change the classification prompt to include other email types like “support”, “complaint”, or “sales”. \n- Add additional logic if you want to CC someone or forward certain types of enquiries. \n- Add a Notion or Google Sheets node to log the conversation for analytics. \n- Replace Gmail with Outlook or another email provider by switching the nodes. \n- Improve context by adding more AI tools like database queries or preloaded FAQs.\n", - "nodes": [ - "n8n-nodes-base.gmailTrigger", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.filter", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "@n8n/n8n-nodes-langchain.toolHttpRequest", - "n8n-nodes-base.gmailTool", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:gmail", "ai", "integration:langchain"], - "triggerType": "gmail", - "hasAI": true, - "score": 85.82, - "scoreBreakdown": { - "traction": 0.541, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/4057", - "author": "yang", - "success": true - }, - { - "id": 3336, - "slug": "automate-blog-content-creation-with-gpt-4-perplexity-wordpre-3336", - "name": "Automate Blog Content Creation with GPT-4, Perplexity & WordPress", - "description": "![Workflow Screenshot](https://www.dr-firas.com/workflow-tool.png) \n\n# Who Is This For\n\nThis workflow is ideal for content creators, solo founders, marketers, and AI enthusiasts who want to automate the full process of blog content creation. \nIt is especially useful for professionals in tech, AI, and automation who publish frequently and need SEO-ready content fast.\n\n# What Problem Does This Workflow Solve\n\n- Creating SEO-optimized blog content is time-consuming and requires consistency. \n- Manually researching trending topics slows down the content pipeline. \n- Formatting, publishing, and promoting across multiple platforms takes effort. \n- This workflow automates the entire process from research to publication.\n\n# What This Workflow Does\n\n- Research: Uses Perplexity AI to gather up-to-date content ideas via form input. \n- Content Generation: GPT-4 creates a short, SEO-optimized article (max 20 lines) with H1, H2 structure and meta-description. \n- Publishing: Automatically posts the content to WordPress. \n- Email Notification: Sends the article title and URL via Gmail. \n- Slack Notification: Notifies a specified Slack channel when the article is live. \n- Database Logging: Saves the article details to a Notion database.\n\n# Setup Guide\n\n## Prerequisites\n\n- WordPress account with API access \n- OpenAI API Key \n- Perplexity API Key \n- Slack Bot Token \n- Notion integration (Database ID) \n- Gmail API credentials (optional) \n- Community Node Required: This workflow uses `n8n-nodes-mcp`, which only works on self-hosted instances of n8n. \n > To install: Go to *Settings > Community Nodes > Install `n8n-nodes-mcp`*\n\n## Steps\n\n1. Import the workflow into your n8n instance \n2. Install the required community node (`n8n-nodes-mcp`) \n3. Set up API credentials for OpenAI, Perplexity, WordPress, Slack, Gmail, and Notion \n4. Customize the form trigger with your preferred prompt \n5. Run a test using a sample topic\n\n# How to Customize This Workflow\n\n- Modify the research prompt to match your niche or industry \n- Adjust GPT-4 settings for tone, structure, or content length \n- Customize Notion fields (e.g., add tags, categories, or labels) \n- Add logic for generating or assigning featured images automatically", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.httpRequest", - "@n8n/n8n-nodes-langchain.chatTrigger", - "n8n-nodes-mcp.mcpClientTool", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.set", - "n8n-nodes-base.wordpressTool", - "n8n-nodes-base.gmailTool", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.formTrigger" - ], - "tags": ["trigger:chatTrigger", "ai", "integration:mcpClientTool"], - "triggerType": "chatTrigger", - "hasAI": true, - "score": 85.81, - "scoreBreakdown": { - "traction": 0.617, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.692 - }, - "source": "https://n8n.io/workflows/3336", - "author": "drfiras", - "success": true - }, - { - "id": 4108, - "slug": "ai-image-generator-from-text-built-on-fal-ai-4108", - "name": "AI image generator from text built on fal.ai", - "description": "## Who this template is for\n\nThis template is for developers, content creators, or application builders who want to integrate an AI-powered text-to-image generation service into their applications or systems via an API endpoint.\n\n## Use case\n\nCreating a secure API endpoint that converts text prompts into AI-generated images, with built-in content moderation to prevent inappropriate content generation. This can be used for creative applications, content creation tools, prototyping interfaces, or any system that needs on-demand image generation.\n\n## How this workflow works\n\n1. Receives text prompt through a webhook endpoint\n2. Filters the prompt for inappropriate content using AI moderation\n3. Submits valid prompts to the Fal.ai Flux image generation service\n4. Polls for completion status and retrieves the generated image when ready\n5. Returns the image results in a structured JSON format to the client\n\n## Set up steps\n\n1. Create a Fal.ai account and obtain API credentials\n2. Configure the HTTP Header Auth credentials with your Fal.ai API key\n3. Set up an OpenAI API key for the content moderation component\n4. Deploy the workflow and note the webhook URL for your API endpoint\n5. Test the endpoint by sending a POST request with a JSON body containing a \"prompt\" field", - "nodes": [ - "n8n-nodes-base.webhook", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.wait", - "n8n-nodes-base.if", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.respondToWebhook", - "@n8n/n8n-nodes-langchain.textClassifier", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:webhook", "ai", "integration:httpRequest"], - "triggerType": "webhook", - "hasAI": true, - "score": 85.79, - "scoreBreakdown": { - "traction": 0.614, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.7 - }, - "source": "https://n8n.io/workflows/4108", - "author": "n8n-team", - "success": true - }, - { - "id": 5157, - "slug": "automate-daily-ai-news-with-perplexity-sonar-pro-via-telegra-5157", - "name": "Automate Daily AI News with Perplexity Sonar Pro (via Telegram)", - "description": "*This workflow contains community nodes that are only compatible with the self-hosted version of n8n.* \n\n🧠 Perplexity-Powered Daily AI News Digest (via Telegram)\n\n\nThis ready-to-deploy n8n workflow automates the entire process of collecting, filtering, formatting, and distributing daily AI industry news summaries directly to your Telegram group or channel.\n\nPowered by Perplexity and OpenAI, it fetches only high-signal AI updates from trusted sources (e.g. OpenAI, DeepMind, HuggingFace, MIT Tech Review), filters out duplicates based on a Google Sheet archive, and delivers beautifully formatted news directly to your team — every morning at 10AM.\n\nFor more such build and step-by-step tutorials, check out:\nhttps://www.youtube.com/@Automatewithmarc\n\n🚀 Key Features:\nPerplexity AI Integration: Automatically fetches the most relevant AI developments from the last 24 hours.\n\nAI Formatter Agent: Cleans the raw feed, removes duplicates, adds summaries, and ensures human-friendly formatting.\n\nGoogle Sheets Log: Tracks previously reported news items to avoid repetition.\n\nTelegram Delivery: Sends a polished daily digest straight to your chat, ready for immediate team consumption.\n\nCustomizable Scheduling: Preconfigured for daily use, but can be modified to fit your team's preferred cadence.\n\n💼 Ideal For:\nAnyone who wants to stay ahead of fast-moving AI trends with zero manual effort\n\n🛠️ Tech Stack:\nPerplexity AI\n\nOpenAI (GPT-4 or equivalent)\n\nGoogle Sheets\n\nTelegram API\n\n✅ Setup Notes:\nYou’ll need to connect your own OpenAI, Perplexity, Google Sheets, and Telegram credentials.\n\nReplace the Google Sheet ID and Telegram channel settings with your own.", - "nodes": [ - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.googleSheetsTool", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.perplexity", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.telegram" - ], - "tags": ["trigger:schedule", "ai", "integration:googleSheetsTool"], - "triggerType": "schedule", - "hasAI": true, - "score": 85.73, - "scoreBreakdown": { - "traction": 0.537, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/5157", - "author": "marconi", - "success": true - }, - { - "id": 3657, - "slug": "build-a-chatbot-voice-and-phone-agent-with-voiceflow-google--3657", - "name": "Build a Chatbot, Voice and Phone Agent with Voiceflow, Google Calendar and RAG", - "description": "**Voiceflow** is a no-code platform that allows you to design, prototype, and deploy conversational assistants across multiple channels—such as **chat**, **voice**, and **phone**—with advanced logic and natural language understanding. It supports integration with APIs, webhooks, and even tools like Twilio for phone agents. It's perfect for building customer support agents, voice bots, or intelligent assistants.\n\nThis workflow connects **n8n** and **Voiceflow** with tools like **Google Calendar**, **Qdrant (vector database)**, **OpenAI**, and an **order tracking API** to power a smart, multi-channel conversational agent.\n\n![image](https://i.postimg.cc/3rSPwMn2/langflow.png)\n\nThere are 3 main webhook endpoints in n8n that Voiceflow interacts with:\n\n1. **`n8n_order`** – receives user input related to order tracking, queries an API, and responds with tracking status.\n2. **`n8n_appointment`** – processes appointment booking, reformats date input using OpenAI, and creates a Google Calendar event.\n3. **`n8n_rag`** – handles general product/service questions using a RAG (Retrieval-Augmented Generation) system backed by:\n - **Google Drive** document ingestion,\n - **Qdrant vector store** for search,\n - and **OpenAI** models for context-based answers.\n\nEach webhook is connected to a corresponding **\"Capture\" block** inside Voiceflow, which sends data to n8n and waits for the response.\n\n---\n\n### **How It Works** \nThis n8n workflow integrates **Voiceflow** for chatbot/voice interactions, **Google Calendar** for appointment scheduling, and **RAG (Retrieval-Augmented Generation)** for knowledge-based responses. Here’s the flow: \n\n- **Trigger**: \n - Three webhooks (`n8n_order`, `n8n_appointment`, `n8n_rag`) receive inputs from Voiceflow (chat, voice, or phone calls). \n - Each webhook routes requests to specific functions: \n - **Order Tracking**: Fetches order status via an external API. \n - **Appointment Scheduling**: Uses OpenAI to parse dates, creates Google Calendar events, and confirms via WhatsApp. \n - **RAG System**: Queries a Qdrant vector store (populated with Google Drive documents) to answer customer questions using GPT-4. \n\n- **AI Processing**: \n - **OpenAI Chains**: Convert natural language dates to Google Calendar formats and generate responses. \n - **RAG Pipeline**: Embeds documents (via OpenAI), stores them in Qdrant, and retrieves context-aware answers. \n - **Voiceflow Integration**: Routes responses back to Voiceflow for multi-channel delivery (chat, voice, or phone). \n\n- **Outputs**: \n - Confirmation messages (e.g., \"Event created successfully\"). \n - Dynamic responses for orders, appointments, or product support. \n\n---\n\n### **Setup Steps** \n\n##### **Prerequisites**: \n- **APIs**: \n - Google Calendar & Drive OAuth credentials. \n - Qdrant vector database (hosted or cloud). \n - OpenAI API key (for GPT-4 and embeddings). \n\n##### **Configuration**: \n1. **Qdrant Setup**: \n - Run the **\"Create collection\"** and **\"Refresh collection\"** nodes to initialize the vector store. \n - Populate it with documents using the **Google Drive → Qdrant** pipeline (embeddings generated via OpenAI). \n\n2. **Voiceflow Webhooks**: \n - Link Voiceflow’s \"Captures\" to n8n’s webhook URLs (`n8n_order`, `n8n_appointment`, `n8n_rag`). \n\n3. **Google Calendar**: \n - Authenticate the **Google Calendar node** and set event templates (e.g., `summary`, `description`). \n\n4. **RAG System**: \n - Configure the **Qdrant vector store** and **OpenAI embeddings** nodes. \n - Adjust the **Retrieve Agent**’s system prompt for domain-specific queries (e.g., electronics store support). \n\n#### **Optional**: \n- Add Twilio for phone-agent capabilities. \n- Customize OpenAI prompts for tone/accuracy. \n\n![image](https://i.postimg.cc/855gyTZP/voiceflow-agent.png)\n![image](https://i.postimg.cc/5Nn4Sk43/voiceflow-agent2.png)\n\nPS. You can import a Twilio number to assign it to your agent for becoming a Phone Agent\n\n![image](https://i.postimg.cc/cLymTTFv/voiceflow-agent3.png)\n\n---\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/)", - "nodes": [ - "n8n-nodes-base.webhook", - "n8n-nodes-base.googleCalendar", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.chainLlm", - "@n8n/n8n-nodes-langchain.vectorStoreQdrant", - "@n8n/n8n-nodes-langchain.embeddingsOpenAi", - "@n8n/n8n-nodes-langchain.toolVectorStore", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.set", - "n8n-nodes-base.respondToWebhook", - "n8n-nodes-base.httpRequest", - "@n8n/n8n-nodes-langchain.outputParserStructured", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.googleDrive", - "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", - "@n8n/n8n-nodes-langchain.textSplitterTokenSplitter" - ], - "tags": ["trigger:webhook", "ai", "integration:langchain"], - "triggerType": "webhook", - "hasAI": true, - "score": 85.67, - "scoreBreakdown": { - "traction": 0.641, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.571 - }, - "source": "https://n8n.io/workflows/3657", - "author": "n3witalia", - "success": true - }, - { - "id": 10358, - "slug": "automate-ai-video-creation-multi-platform-publishing-with-gp-10358", - "name": "Automate AI Video Creation & Multi-Platform Publishing with GPT-4, Veo 3.1 & Blotato", - "description": "# 💥 Automate AI Video Creation & Multi-Platform Publishing with Veo 3.1 & Blotato\n\n![Workflow Overview](https://www.dr-firas.com/veo31.png)\n\n### 🎯 Who is this for?\nThis workflow is designed for **content creators, marketers, and automation enthusiasts** who want to produce professional AI-generated videos and publish them automatically on social media — without editing or manual uploads. Perfect for those using **Veo 3.1**, **GPT-4**, and **Blotato** to scale video creation.\n\n---\n\n### 💡 What problem is this workflow solving?\nCreating short-form content (TikTok, Instagram Reels, YouTube Shorts) is time-consuming — from writing scripts to video editing and posting. \nThis workflow eliminates the manual steps by combining **AI storytelling + video generation + automated publishing**, letting you focus on creativity while your system handles production and distribution.\n\n---\n\n### ⚙️ What this workflow does\n1. **Reads new ideas from Google Sheets** \n2. **Generates story scripts** using GPT-4 \n3. **Creates cinematic videos** using Veo 3.1 (`fal.ai/veo3.1/reference-to-video`) with 3 input reference images \n4. **Uploads the final video** automatically to Google Drive \n5. **Publishes the video** across multiple platforms (TikTok, Instagram, Facebook, X, LinkedIn, YouTube) via **Blotato** \n6. **Updates Google Sheets** with video URL and status (Completed / Failed)\n\n---\n\n### 🧩 Setup\n**Required accounts:**\n- [OpenAI](https://platform.openai.com/api-keys) → GPT-4 API key \n- [fal.ai](https://fal.ai/dashboard/keys) → Veo 3.1 API key \n- [Google Cloud Console](https://console.cloud.google.com) → Sheets & Drive connection \n- [Blotato](https://blotato.com/?ref=firas) → API key for social media publishing \n\n**Configuration steps:**\n1. Copy the **Google Sheets structure**: \n - A: `id_video` \n - B: `niche` \n - C: `idea` \n - D: `url_1` \n - E: `url_2` \n - F: `url_3` \n - G: `url_final` \n - H: `status` \n\n2. Add your API keys to the **Workflow Configuration** node. \n3. Insert three image URLs and a short idea into your sheet. \n4. Wait for the automation to process and generate your video. \n\n---\n\n### 🧠 How to customize this workflow\n- **Change duration or aspect ratio** → Edit the Veo 3.1 node JSON body (`duration`, `aspect_ratio`) \n- **Modify prompt style** → Adjust the “Optimize Prompt for Veo” node for your desired tone or cinematic look \n- **Add more platforms** → Extend Blotato integration to publish on Pinterest, Reddit, or Threads \n- **Enable Telegram Trigger** → Allow users to submit ideas and images directly via Telegram \n\n---\n\n### 🚀 Expected Outcome\nWithin **2–3 minutes**, your idea is transformed into a full cinematic AI video — complete with storytelling, visuals, and automatic posting to your social media channels. \n\nSave hours of editing and focus on strategy, creativity, and growth.\n\n---\n\n### 👋 Need help or want to customize this?\n📩 Contact: [LinkedIn](https://www.linkedin.com/in/dr-firas/) \n📺 YouTube: [@DRFIRASS](https://www.youtube.com/@DRFIRASS) \n🚀 Workshops: [Mes Ateliers n8n](https://hotm.art/formation-n8n)\n\n---\n📄 **Documentation**: [Notion Guide](https://automatisation.notion.site/Automate-AI-Video-Creation-Multi-Platform-Publishing-with-Veo-3-1-Blotato-29c3d6550fd981cd982fd4a031bdfe07?source=copy_link)\n### Need help customizing?\nContact me for consulting and support : [Linkedin](https://www.linkedin.com/in/dr-firas/) / [Youtube](https://www.youtube.com/channel/UCriIQI8uaoEro5FEnOpeidQ) / [🚀 Mes Ateliers n8n ](https://hotm.art/formation-n8n)", - "nodes": [ - "n8n-nodes-base.set", - "n8n-nodes-base.code", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.googleDrive", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.googleSheetsTrigger", - "n8n-nodes-base.stickyNote", - "@blotato/n8n-nodes-blotato.blotato", - "n8n-nodes-base.merge" - ], - "tags": ["trigger:other", "ai", "integration:blotato"], - "triggerType": "other", - "hasAI": true, - "score": 85.6, - "scoreBreakdown": { - "traction": 0.673, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.429 - }, - "source": "https://n8n.io/workflows/10358", - "author": "drfiras", - "success": true - }, - { - "id": 4573, - "slug": "google-maps-business-scraper-with-contact-extraction-via-api-4573", - "name": "Google Maps business scraper with contact extraction via Apify and Firecrawl", - "description": "## Who is this for?\nMarketing agencies, sales teams, lead generation specialists, and business development professionals who need to build comprehensive business databases with contact information for outreach campaigns across any industry.\n\n## What problem is this workflow solving?\nFinding businesses and their contact details manually is time-consuming and inefficient. This workflow automates the entire process of discovering businesses through Google Maps and extracting their digital contact information from websites, saving hours of manual research.\n\n## What this workflow does\nThis automated workflow runs every 30 minutes to:\n- Scrape business data from Google Maps using Apify's Google Places crawler\n- Save basic business information (name, address, phone, website) to Google Sheets\n- Filter businesses that have websites\n- Scrape each business's website content using Firecrawl\n- Extract contact information including emails, LinkedIn, Facebook, Instagram, and Twitter profiles\n- Store all extracted data in organized Google Sheets for easy access and follow-up\n\n## Setup\n**Required Services:**\n- Google Sheets account with OAuth2 setup\n- Apify account with API access for Google Places scraping\n- Firecrawl account with API access for website scraping\n\n**Pre-setup:**\n1. Copy this [Google Sheet](https://docs.google.com/spreadsheets/d/1DHezdcetT0c3Ie1xB3z3jDc5WElsLN87K4J9EQDef9g/edit?usp=sharing)\n2. Configure your Apify and Firecrawl API credentials in n8n\n3. Set up Google Sheets OAuth2 connection\n4. Update the Google Sheet ID in all Google Sheets nodes\n\n**Quick Start:**\nThe workflow includes detailed sticky notes explaining each phase. Simply configure your API credentials and Google Sheet, then activate the workflow.\n\n## How to customize this workflow to your needs\n- **Change search criteria**: Modify the Apify scraping parameters to target different business types (restaurants, gyms, salons, etc.) or locations\n- **Adjust schedule**: Change the trigger interval from 30 minutes to your preferred frequency \n- **Add more contact fields**: Extend the extraction code to find additional contact information like WhatsApp or Telegram\n- **Filter criteria**: Modify the filter conditions to target businesses with specific characteristics\n- **Batch size**: Adjust the batch processing to handle more or fewer websites simultaneously\n\nPerfect for lead generation, competitor research, and building targeted marketing lists across any industry or business type.", - "nodes": [ - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.if", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.wait", - "n8n-nodes-base.filter", - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.code", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:schedule", "integration:googleSheets"], - "triggerType": "schedule", - "hasAI": false, - "score": 85.48, - "scoreBreakdown": { - "traction": 0.631, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.571 - }, - "source": "https://n8n.io/workflows/4573", - "author": "n8nstein", - "success": true - }, - { - "id": 5303, - "slug": "google-search-console-and-analytics-analysis-with-ai-optimiz-5303", - "name": "Google Search Console and Analytics Analysis with AI Optimizations", - "description": "## What Is This?\n\nThis workflow is a comprehensive solution for automating website audits and optimizations, leveraging advanced technologies to boost SEO effectiveness and overall site performance.\n\n## Who Is It For?\n\nDesigned for SEO specialists, digital marketers, webmasters, and content teams, this workflow empowers anyone responsible for website performance to automate and scale their audit processes. Agencies managing multiple client sites, in-house SEO teams aiming to save time on routine checks, and developers seeking to integrate data-driven insights into their deployment pipelines will all find this solution invaluable.\n\nBy combining your site’s sitemap with Google Search Console and Google Analytics data, then applying AI-powered analysis, the workflow continuously uncovers actionable recommendations to boost search visibility, improve user engagement, and accelerate page performance. Whether you manage a single blog or oversee a sprawling e-commerce platform, this automated pipeline delivers precise, prioritized SEO improvements without manual data wrangling.\n\n## How Does It Work?\n\nThis end-to-end site analysis automation consists of five main stages:\n\n**1. URL Discovery** \nProcesses the sitemap.xml using HTTP Request and XML nodes to extract all site URLs.\n\n**2. Search Console Performance Analysis** \nUses the Google Search Console API to fetch detailed metrics for each page, including search position, clicks, impressions, and CTR. \n\n**3. Analytics Data Collection** \nConnects to the Google Analytics API to automatically retrieve traffic metrics such as pageviews, average session duration, bounce rate, and conversions. \n\n**4. AI Data Processing** \nEmploys OpenAI models to perform in-depth analysis of the collected data. The artificial intelligence engine merges insights from all sources, identifies patterns, and produces detailed optimization recommendations. AI analyses website itsefl aswell. **Consider testing different models**. I do recommend at least trying out o4-mini.\n\n**5. Recommendation Generation** \nCreates tailored suggestions for each page, in form of HTML table, that is being sent to your email.\n\n## How To Set It Up?\n\n**Accounts:** \nAn active n8n account or instance, API keys for Google Search Console and Google Analytics, an OpenAI access token.\n\n**Enabled Google APIs:**\nYou will neeed at least following scopes:\n- Google Search Console API\n- Google Analytics Aadmin API\n- Google Analytics Data API \n\n**Scheduling:** \nThe workflow can run manually for ad hoc audits or be scheduled (daily, weekly) for continuous site monitoring.\n\n**Testing:** \nThere are two nodes that are optional:\n- \"Sort for testing purposes\" and\n- \"Limit for testing purposes\"\nTogether they randomly select items from sitemap and limit them to few so you don't need to run hundreds of sitemap.xml items at once, but you can run just a random batch first.\n\n**Globals:**\nThere is node called \"Globals- CHANGE ME!\". You need to set up proper variables in there, which are:\n- sitemap_url - self exlpainatory\n- search_console_selector - for example \"sc-domain:sailingbyte.com\" but can be URL aswell- depends on how did you set up your search console\n- analysis_start_date and analysis_end_date - date range for analytics, by default last 30 days\n- analytics_selector_id - ID of Google Analytics setup, it is a large integer, you can find it in analytics url preceeded with letter \"p\", ex (your number is where there are X's): https://analytics.google.com/analytics/web/#/pXXXXXXXXX/reports/intelligenthome\n- report_receiver - email which will receive report \n\n## What's More?\n\nThat's actually it. I hope that this automation will help your website improvement will be much easier!\n\n## Thank you, perfect!\n\nGlad I could help. Visit my profile for other automations for businesses. And if you are looking for dedicated software development, do not hesitate to reach out!", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.merge", - "n8n-nodes-base.removeDuplicates", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.xml", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.limit", - "n8n-nodes-base.wait", - "n8n-nodes-base.set", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.googleAnalytics", - "n8n-nodes-base.httpRequestTool", - "n8n-nodes-base.markdown", - "n8n-nodes-base.html", - "n8n-nodes-base.emailSend", - "n8n-nodes-base.sort", - "@n8n/n8n-nodes-langchain.agent" - ], - "tags": ["trigger:manual", "ai", "integration:merge"], - "triggerType": "manual", - "hasAI": true, - "score": 85.4, - "scoreBreakdown": { - "traction": 0.574, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.783 - }, - "source": "https://n8n.io/workflows/5303", - "author": "lukaszpp", - "success": true - }, - { - "id": 11047, - "slug": "automated-ai-music-generation-with-elevenlabs-google-sheets--11047", - "name": "Automated 🤖🎵 AI Music Generation with ElevenLabs, Google Sheets & Drive", - "description": "🤖🎵 This workflow automates the creation, storage, and cataloging of AI-generated music using the **[Eleven Music API](https://try.elevenlabs.io/ahkbf00hocnu)**, **Google Sheets**, and **Google Drive**.\n\n---\n### **Key Advantages**\n✅ **Fully Automated Music Generation Pipeline**\n\nOnce started, the workflow automatically:\n\n* Reads track parameters\n* Generates music via API\n* Uploads the file\n* Updates your spreadsheet\n No manual steps needed after initialization.\n\n✅ **Centralized Track Management**\n\nA single Google Sheet acts as your **project control center**, letting you organize:\n\n* Prompts\n* Durations\n* Generated URLs\n This avoids losing track of files and creates a ready-to-share catalog.\n\n✅ **Seamless Integration with Google Services**\n\nThe workflow:\n\n* Reads instructions from **Google Sheets**\n* Saves the MP3 to **Google Drive**\n* Updates the same Sheet with the final link\n This ensures everything stays synchronized and easy to access.\n\n✅ **Scalable and Reliable Processing**\n\nThe loop-with-delay mechanism:\n\n* Processes tracks sequentially\n* Prevents API overload\n* Ensures stable execution\n This is especially helpful when generating multiple long tracks.\n\n✅ **Easy Customization**\n\nBecause the prompts and durations come from Google Sheets:\n\n* You can edit prompts at any time\n* You can add more tracks without modifying the workflow\n* You can clone the Sheet for different projects\n\n✅ **Ideal for Creators and Businesses**\n\nThis workflow is perfect for:\n\n* Content creators generating background music\n* Agencies designing custom soundtracks\n* Businesses needing AI-generated audio assets\n* Automated production pipelines\n\n---\n\n### How It Works\n\nThe process operates as follows:\n\n- The workflow starts manually via the \"Execute workflow\" trigger\n- It retrieves a list of music track requests from a Google Sheets spreadsheet containing track titles, text prompts, and duration specifications\n- The system processes each track request individually through a batch loop\n- For each track, it sends the text prompt and duration to ElevenLabs Music API to generate studio-quality music\n- The generated MP3 file (in 44100 Hz, 128 kbps format) is automatically uploaded to a designated Google Drive folder\n- Once uploaded, the workflow updates the original Google Sheets with the direct URL to the generated music file\n- A 1-minute wait period between each track generation prevents API rate limiting\n- The process continues until all track requests in the spreadsheet have been processed\n\n---\n\n### Set Up Steps\n\n**Prerequisites:**\n- ElevenLabs paid account with Music API access enabled\n- Google Sheets spreadsheet with specific columns: TITLE, PROMPT, DURATION (ms), URL\n- Google Drive folder for storing generated music files\n\n**Configuration Steps:**\n\n1. **ElevenLabs API Setup:**\n - Enable Music Generation access in your [ElevenLabs account](https://try.elevenlabs.io/ahkbf00hocnu)\n - Generate an API key from the ElevenLabs developer dashboard\n - Configure HTTP Header authentication in n8n with name \"xi-api-key\" and your API value\n\n2. **Google Sheets Preparation:**\n - Create or clone the music tracking spreadsheet with required columns\n - Fill in track titles, detailed text prompts, and durations in milliseconds (10,000-300,000 ms)\n - Configure Google Sheets OAuth credentials in n8n\n - Update the document ID in the Google Sheets nodes\n\n3. **Google Drive Configuration:**\n - Create a dedicated folder for music uploads\n - Set up Google Drive OAuth credentials in n8n\n - Update the folder ID in the upload node\n\n4. **Workflow Activation:**\n - Ensure all API credentials are properly configured\n - Test with a single track entry in the spreadsheet\n - Verify music generation, upload, and spreadsheet update work correctly\n - Execute the workflow to process all pending track requests\n\nThe workflow automatically names files with timestamp prefixes (song_yyyyMMdd) and handles the complete lifecycle from prompt to downloadable music file.\n\n---\n\n👉 [Subscribe to my new **YouTube channel**](https://youtube.com/@n3witalia). Here I’ll share videos and Shorts with practical tutorials and **FREE templates for n8n**.\n\n[![image](https://n3wstorage.b-cdn.net/n3witalia/youtube-n8n-cover.jpg)](https://youtube.com/@n3witalia)\n\n---\n\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/).", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.wait", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.googleDrive" - ], - "tags": ["trigger:manual", "integration:googleSheets"], - "triggerType": "manual", - "hasAI": false, - "score": 85.32, - "scoreBreakdown": { - "traction": 0.551, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.857 - }, - "source": "https://n8n.io/workflows/11047", - "author": "n3witalia", - "success": true - }, - { - "id": 4457, - "slug": "ai-telegram-bot-agent-smart-assistant-content-summarizer-4457", - "name": "AI Telegram Bot Agent: Smart Assistant & Content Summarizer", - "description": "\n**Create your own intelligent Telegram bot that summarizes articles and processes commands automatically.**\n\nThis powerful workflow turns Telegram into your personal AI assistant, handling `/help`, `/summary <URL>`, and `/img <prompt>` commands with intelligent responses - perfect for teams, content creators, and anyone wanting smart automation in their messaging.\n\n## 🚀 What It Does\n\n**Smart Command Processing**: Automatically recognizes and routes `/help`, `/summary`, and `/img` commands to appropriate AI-powered responses.\n\n**Article Summarization**: Fetches any URL, extracts content, and generates professional 10-12 bullet point summaries using OpenAI.\n\n**Image Generation**: Processes image prompts and integrates with AI image generation services.\n\n**Help System**: Provides users with clear command instructions and usage examples.\n\n## 🎯 Key Benefits\n\n✅ **Personal AI Assistant**: Get instant article summaries in Telegram \n✅ **Team Productivity**: Share quick content summaries with colleagues \n✅ **Content Research**: Rapidly digest articles and web content \n✅ **24/7 Availability**: Bot works around the clock without maintenance \n✅ **Easy Commands**: Simple `/summary <link>` format anyone can use \n✅ **Scalable**: Handles multiple users and requests simultaneously \n\n## 🏢 Perfect For\n\n### **Content Teams & Researchers**\n- Journalists quickly summarizing news articles\n- Marketing teams researching competitor content\n- Students processing academic papers and articles\n- Analysts digesting industry reports\n\n### **Business Applications**\n- **Team Communication**: Share article insights in group chats\n- **Research Assistance**: Quick content analysis for decision making\n- **Content Curation**: Summarize articles for newsletters or reports\n- **Knowledge Sharing**: Help teams stay informed efficiently\n\n## ⚙️ What's Included\n\n**Complete Bot Workflow**: Ready-to-deploy Telegram bot with all commands\n**AI Integration**: OpenAI-powered content summarization and processing\n**Smart Routing**: Intelligent command recognition and response system\n**Error Handling**: Robust system handles invalid commands gracefully\n**Extensible Design**: Easy to add new commands and features\n\n## 🔧 Quick Setup Requirements\n\n- **n8n Platform**: Cloud or self-hosted instance\n- **Telegram Bot Token**: Create via @BotFather (free, 5 minutes)\n- **OpenAI API**: For content summarization (pay-per-use)\n- **Basic Configuration**: Follow 15-minute setup guide\n\n## 📱 User Experience\n\n**Simple Commands:**\n```\n/help - Show available commands\n/summary https://example.com - Get article summary\n/img sunset over mountains - Generate image (with supported APIs)\n```\n\n**Sample Summary Output:**\n```\n📄 Article Summary:\n\n• Company reports 40% revenue growth in Q3 2024\n• New AI features driving customer acquisition\n• Expansion into European markets planned for 2025\n• Investment in R&D increased by 25% this quarter\n• Customer satisfaction scores improved to 94%\n• Three new product launches scheduled for next year\n• Remote work policy made permanent post-pandemic\n• Sustainability goals on track to meet 2025 targets\n• Partnership with major tech firm announced\n• Stock price up 15% following earnings report\n```\n\n## 🎨 Customization Options\n\n**Command Extensions**: Add custom commands for specific workflows\n**Response Formatting**: Customize summary style and length\n**Multi-Language**: Support different languages for international teams\n**Integration APIs**: Connect additional AI services and tools\n**User Permissions**: Control who can use specific commands\n**Analytics**: Track usage patterns and popular content\n\n## 🏷️ Tags & Categories\n\n`#telegram-bot` `#ai-automation` `#content-summarization` `#article-processing` `#team-productivity` `#openai-integration` `#smart-assistant` `#workflow-automation` `#messaging-bot` `#content-research` `#ai-agent` `#n8n-workflow` `#business-automation` `#telegram-integration` `#ai-powered-bot`\n\n## 💡 Use Case Examples\n\n**News Team**: Quickly summarize breaking news articles for editorial meetings\n**Marketing Agency**: Research competitor content and industry trends efficiently \n**Sales Team**: Digest industry reports and share insights with prospects\n**Remote Team**: Keep everyone informed with summarized company updates\n\n## 📈 Expected Results\n\n- **80% faster** content research and analysis\n- **50% more articles** processed per day vs manual reading\n- **100% team accessibility** through familiar Telegram interface\n- **24/7 availability** for global teams across time zones\n\n## 🛠️ Setup & Support\n\n**Quick Start**: Deploy your bot in 15 minutes with included guide\n**Video Tutorial**: Complete walkthrough available\n**Template Commands**: Pre-built responses and formatting\n**Expert Support**: Direct help from workflow creator\n\n## 📞 Get Help & Resources\n\nYouTube: [https://www.youtube.com/@YaronBeen/videos](https://www.youtube.com/@YaronBeen/videos)\n\n**💼 Professional Support** \nLinkedIn: [https://www.linkedin.com/in/yaronbeen/](https://www.linkedin.com/in/yaronbeen/)\n\n**📧 Direct Help** \nEmail: Yaron@nofluff.online - Response within 24 hours\n\n---\n\n**Ready to build your intelligent Telegram assistant?**\n\nGet this AI Telegram Bot Agent and transform your messaging app into a powerful content processing tool. Perfect for teams, researchers, and anyone who wants AI-powered assistance directly in Telegram.\n\n*Stop manually reading long articles. Start getting instant, intelligent summaries with simple commands.*", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.telegramTrigger", - "n8n-nodes-base.if", - "n8n-nodes-base.telegram", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.html", - "@n8n/n8n-nodes-langchain.openAi" - ], - "tags": ["trigger:telegram", "ai", "integration:if"], - "triggerType": "telegram", - "hasAI": true, - "score": 85.31, - "scoreBreakdown": { - "traction": 0.629, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.545 - }, - "source": "https://n8n.io/workflows/4457", - "author": "yaron-nofluff", - "success": true - }, - { - "id": 3669, - "slug": "publish-image-video-to-multiple-social-media-x-instagram-fac-3669", - "name": "Publish image & video to multiple social media (X, Instagram, Facebook and more)", - "description": "This Workflow streamlines the process of publishing posts (**image or video**) to multiple social media platforms using a unified form and a third-party API service called Upload-Post.\n\nThe automation starts with a form trigger, allowing users to submit content (text and media) through a simple frontend interface. Users select the platform (**Instagram, LinkedIn, Facebook, X, TikTok, Threads**), choose the profile name, write a caption, and upload a photo or video.\n\n---\n\n### **How It Works** \nAutomates cross-platform social media posting via Upload-Post, handling both **images** (JPEG) and **videos** (MP4). Here’s the process: \n\n- **Trigger**: \n - A **form submission** captures user inputs: \n - **Platform** (Instagram, LinkedIn, Facebook, X, TikTok, Threads). \n - **Account** (pre-configured profile name). \n - **Caption** and **file** (image/video). \n - Optional **Facebook Page ID** for targeted posting. \n\n- **Routing**: \n - The **\"Video or Photo?\"** Switch node checks the file’s MIME type: \n - **Image**: Routes to the **\"Post photo\"** HTTP node (uploads via `upload_photos` API). \n - **Video**: Routes to the **\"Post video\"** HTTP node (uploads via `upload` API). \n\n- **API Integration**: \n - Both nodes send data to Upload-Post.com’s API, including: \n - Caption, account name, platform, and file binary. \n - Facebook ID (if provided). \n\n- **Success/Failure Handling**: \n - The **\"Result Photo/Video\"** nodes parse the API response. \n\n---\n\n### **Setup Steps** \n\n##### **Prerequisites**: \n- **Upload-Post.com API Key**: \n - Get it from the [API Keys dashboard](https://www.upload-post.com/?linkId=lp_144414&sourceId=n3witalia&tenantId=upload-post-app). \n - Free tier allows **10 uploads/month**. \n\n##### **Configuration**: \n1. **API Authentication**: \n - In the **HTTP Request nodes** (`Post photo`/`Post video`), set the `Authorization` header: \n - `Name`: `Authorization` \n - `Value`: `Apikey YOUR_API_KEY_HERE`. \n\n2. **Form Customization**: \n - Adjust the **\"On form submission\"** node to: \n - Add/remove platforms (e.g., YouTube when approved). \n - Modify file type restrictions (default: `.jpg`, `.mp4`). \n\n3. **Account Mapping**: \n - Ensure the **\"Account\"** field matches profiles configured in Upload-Post.com (e.g., `test1`, `test2`). \n\n4. **Facebook Page Integration**: \n - Optional: Add a **Facebook Page ID** field for page-specific posts. \n\n5. **Testing**: \n - Submit test forms with images/videos. \n - Verify API responses and success/failure messages. \n\n##### **Optional Enhancements**: \n- Add **error logging** (e.g., save failed attempts to a database). \n- Extend to **YouTube** once API support is confirmed. \n\n---\n\n### **Key Features**: \n- **Multi-Platform**: Post to 6+ social networks simultaneously. \n- **User-Friendly**: Simple form interface for non-technical users. \n- **Error Handling**: Clear feedback for success/failure cases. \n\n---\n\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/). ", - "nodes": [ - "n8n-nodes-base.formTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.form", - "n8n-nodes-base.if", - "n8n-nodes-base.set", - "n8n-nodes-base.switch", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:formTrigger", "integration:form"], - "triggerType": "formTrigger", - "hasAI": false, - "score": 85.3, - "scoreBreakdown": { - "traction": 0.64, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.5 - }, - "source": "https://n8n.io/workflows/3669", - "author": "n3witalia", - "success": true - }, - { - "id": 4731, - "slug": "automated-investor-intelligence-crunchbase-to-google-sheets--4731", - "name": " Automated Investor Intelligence: CrunchBase to Google Sheets Data Harvester", - "description": "# 🚀 Automated Investor Intelligence: CrunchBase to Google Sheets Data Harvester!\n\n## Workflow Overview\nThis cutting-edge n8n automation is a sophisticated investor intelligence tool designed to transform market research into actionable insights. By intelligently connecting CrunchBase, data processing, and Google Sheets, this workflow:\n\n1. **Discovers Investor Insights**: \n - Automatically retrieves latest investor data\n - Tracks key investment organizations\n - Eliminates manual market research efforts\n\n2. **Intelligent Data Processing**:\n - Filters investor-specific organizations\n - Extracts critical investment metrics\n - Ensures comprehensive market intelligence\n\n3. **Seamless Data Logging**:\n - Automatically updates Google Sheets\n - Creates real-time investor database\n - Enables rapid market trend analysis\n\n4. **Scheduled Intelligence Gathering**:\n - Daily automated tracking\n - Consistent investor insight updates\n - Zero manual intervention required\n\n## Key Benefits\n- 🤖 **Full Automation**: Zero-touch investor research\n- 💡 **Smart Filtering**: Targeted investment insights\n- 📊 **Comprehensive Tracking**: Detailed investor intelligence\n- 🌐 **Multi-Source Synchronization**: Seamless data flow\n\n## Workflow Architecture\n\n### 🔹 Stage 1: Investor Discovery\n- **Scheduled Trigger**: Daily market scanning\n- **CrunchBase API Integration**\n- **Intelligent Filtering**:\n - Investor-specific organizations\n - Key investment metrics\n - Most recent data\n\n### 🔹 Stage 2: Data Extraction\n- **Comprehensive Metadata Parsing**\n- **Key Information Retrieval**\n- **Structured Data Preparation**\n\n### 🔹 Stage 3: Data Logging\n- **Google Sheets Integration**\n- **Automatic Row Appending**\n- **Real-Time Database Updates**\n\n## Potential Use Cases\n- **Venture Capitalists**: Investment ecosystem mapping\n- **Startup Scouts**: Investor trend analysis\n- **Market Researchers**: Comprehensive investment insights\n- **Business Development**: Strategic partnership identification\n- **Investment Analysts**: Market intelligence gathering\n\n## Setup Requirements\n1. **CrunchBase API**\n - API credentials\n - Configured access permissions\n - Investor organization tracking setup\n\n2. **Google Sheets**\n - Connected Google account\n - Prepared tracking spreadsheet\n - Appropriate sharing settings\n\n3. **n8n Installation**\n - Cloud or self-hosted instance\n - Workflow configuration\n - API credential management\n\n## Future Enhancement Suggestions\n- 🤖 Advanced investment trend analysis\n- 📊 Multi-source investor aggregation\n- 🔔 Customizable alert mechanisms\n- 🌐 Expanded investment stage tracking\n- 🧠 Machine learning insights generation\n\n## Technical Considerations\n- Implement robust error handling\n- Use secure API authentication\n- Maintain flexible data processing\n- Ensure compliance with API usage guidelines\n\n## Ethical Guidelines\n- Respect business privacy\n- Use data for legitimate research\n- Maintain transparent information gathering\n- Provide proper attribution\n\n## Hashtag Performance Boost 🚀\n#InvestorIntelligence #VentureCapital #MarketResearch #AIWorkflow #DataAutomation #StartupEcosystem #InvestmentTracking #BusinessIntelligence #TechInnovation #StartupFunding\n\n## Workflow Visualization\n\n```plaintext\n[Daily Trigger]\n ⬇️\n[Fetch Investor Data]\n ⬇️\n[Extract Investor Fields]\n ⬇️\n[Log to Google Sheets]\n```\n\n## Connect With Me\n\n**Ready to revolutionize your investor research?**\n\n📧 **Email**: Yaron@nofluff.online\n\n🎥 **YouTube**: [@YaronBeen](https://www.youtube.com/@YaronBeen/videos)\n\n💼 **LinkedIn**: [Yaron Been](https://www.linkedin.com/in/yaronbeen/)\n\n**Transform your market intelligence with intelligent, automated workflows!**", - "nodes": [ - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.code", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:schedule", "integration:httpRequest"], - "triggerType": "schedule", - "hasAI": false, - "score": 85.26, - "scoreBreakdown": { - "traction": 0.513, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/4731", - "author": "yaron-nofluff", - "success": true - }, - { - "id": 4849, - "slug": "automated-invoice-processing-with-telegram-gpt-4o-ocr-and-sa-4849", - "name": "Automated Invoice Processing with Telegram, GPT-4o, OCR and SAP Integration", - "description": "**++HOW IT WORKS:++**\nThis workflow automates the processing of invoices sent via Telegram. It extracts the data using LlamaIndex OCR, logs it in Google Sheets, and optionally pushes the structured data to SAP Business One\n\n****🔹 1. Receive Invoice via Telegram:****\n- A user sends a PDF of an invoice through Telegram\n- A Telegram Trigger node listens for incoming messages and captures the file and metadata\n- The document is downloaded and prepared for OCR\n\n**🔹 2. OCR with LlamaIndex:**\n- The file is uploaded to the LlamaIndex OCR API.\n- The workflow polls the API until the processing status returns SUCCESS\n- Once ready, the parsed content is fetched in Markdown format\n\n**🔹 3. Data Extraction via LLM (editable):**\n- The Markdown content is sent to a language model (LLM) using LangChain\n- A Structured Output Parser transforms the result into a clean, structured editable JSON\n\n**🔹 4. Save to Google Sheets:**\nThe structured JSON is split into:\n1. Header (main invoice metadata)\n1. Detail (individual line items)\n\nEach part is stored in a dedicated tab within a connected Google Sheets file\n\n**🔹 5. Ask for SAP Confirmation:**\nThe bot replies to the user via Telegram:\n\n\"Do you want to send the data to SAP?\"\n\nIf the user clicks \"Yes\", the next automation path is triggered.\n\n**🔹 6. Push Data to SAP B1:**\nA connection is made to SAP Business One's Service Layer API\n\nHeader and detail data are fetched from Google Sheets\n\nThe invoice structure is rebuilt as required by SAP (DocumentLines, CardCode, etc.)\n\nA POST request creates the Purchase Invoice in SAP\n\nA confirmation message with the created DocEntry is sent back to the user on Telegram\n\n**++SET UP STEPS:++**\nFollow these steps to properly configure the workflow before execution:\n\n**1️⃣ Create Required Credentials:**\nGo to Credentials > + New Credential and create the following:\n- *Telegram API* (set your bot token get it from BotFather)\n- *Google Sheets*\n- *OpenAI*\n\n**2️⃣ Set Up Environment Variables (Optional but Recommended):**\nLLAMAINDEX_API_KEY\nSAP_USER\nSAP_PASSWORD\nSAP_COMPANY_DB\nSAP_URL\n\n**3️⃣ Prepare Google Sheets:**\nEnsure your Google Spreadsheet has the following:\n➤ Sheet 1: Header\n➤ Sheet 2: Details\nContains columns for invoice lines", - "nodes": [ - "n8n-nodes-base.wait", - "@n8n/n8n-nodes-langchain.chainLlm", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.noOp", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.merge", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.telegramTrigger", - "n8n-nodes-base.switch", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.code", - "n8n-nodes-base.telegram", - "n8n-nodes-base.set", - "@n8n/n8n-nodes-langchain.outputParserStructured" - ], - "tags": ["trigger:telegram", "ai", "integration:httpRequest"], - "triggerType": "telegram", - "hasAI": true, - "score": 85.21, - "scoreBreakdown": { - "traction": 0.631, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.519 - }, - "source": "https://n8n.io/workflows/4849", - "author": "raquelgiugliano", - "success": true - }, - { - "id": 4596, - "slug": "qr-code-generator-via-webhook-4596", - "name": "QR Code Generator via Webhook", - "description": "This n8n template allows you to instantly generate QR codes from any text or URL by simply sending a webhook request. It's a versatile tool for creating dynamic QR codes for various purposes, from marketing campaigns to event registrations, directly integrated into your automated workflows.\n\n---\n\n# 🔧 How it works\n- Receive Data Webhook: This node acts as the entry point for the workflow. It listens for incoming POST requests and expects a JSON body with a data property containing the text or URL you want to encode into the QR code.\n- Generate QR Code: This node makes an HTTP GET request to the QR Server API (api.qrserver.com) to generate the QR code image. The content from your webhook is passed as the data parameter to the API.\n- Respond with QR Code: This node sends the response from the QR Server API back to the service that initiated the webhook. The QR Server API directly returns the image data, so your webhook response will be the QR code image itself.\n\n---\n\n# 👤 Who is it for?\n### This workflow is ideal for:\n\n- Marketers: Generate QR codes for product links, event registrations, or promotional materials on the fly.\n- Developers: Integrate QR code generation into applications, websites, or internal tools.\n- Event Organizers: Create dynamic QR codes for ticketing, information access, or check-ins.\n- Businesses: Streamline processes requiring physical-to-digital transitions, like menu access or contact sharing.\n- Automation Enthusiasts: Add QR code generation capabilities to any workflow.\n\n---\n\n# 📑 Data Structure\nWhen you trigger the webhook, send a POST request with a JSON body structured as follows:\n\n```\n{\n \"data\": \"https://www.yourwebsite.com/your-specific-page-or-text-to-encode\"\n}\n```\n\nThe workflow will return the QR code image directly in the response.\n\n---\n\n# ⚙️ Setup Instructions\n- Import Workflow: In your n8n editor, click \"Import from JSON\" and paste the provided workflow JSON.\n- Configure Webhook Path:\n - Double-click the Receive Data Webhook node.\n - In the 'Path' field, set a unique and descriptive path (e.g., /generate-qr).\n- Customize QR Code (Optional):\n - Double-click the Generate QR Code node.\n - You can adjust the size parameter in the URL (e.g., size=200x200 for a larger QR code) or add other parameters supported by the QR Server API (e.g., bgcolor, color, qzone).\n- Activate Workflow: Save and activate the workflow.\n\n---\n\n# 📝 Tips\n- Handling the Image Output: Since the QR Server API directly returns the image, the webhook response will be the image data. Depending on your use case, you might want to:\n - Save to File/Cloud: Insert a node (e.g., Write Binary File, Amazon S3, Google Drive) after Generate QR Code to save the image to a file system or cloud storage.\n - Embed in HTML/Email: If you're building an HTML response or sending an email, you might need to convert the image data to a Base64 string or provide a URL to a saved image.\n- Error Handling: Enhance workflow robustness by adding an Error Trigger node. This allows you to catch any issues during QR code generation and set up notifications or logging.\n- Dynamic Size/Color: You can extend the Receive Data Webhook to accept parameters for size, color, or bgcolor in the incoming JSON. Then, dynamically pass these to the url of the Generate QR Code node to create highly customizable QR codes.\n- Input Validation: For more advanced use cases, you could add a Function node after the webhook to validate the incoming data to ensure it's in a valid format (e.g., a URL).", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.webhook", - "n8n-nodes-base.respondToWebhook" - ], - "tags": ["trigger:webhook", "integration:httpRequest"], - "triggerType": "webhook", - "hasAI": false, - "score": 85.15, - "scoreBreakdown": { - "traction": 0.508, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/4596", - "author": "ist00dent", - "success": true - }, - { - "id": 4887, - "slug": "auto-create-podcast-from-youtube-transcript-using-dumpling-a-4887", - "name": "Auto-Create Podcast from YouTube Transcript using Dumpling AI and GPT-4o", - "description": "### 🔎 Who is this for?\nThis workflow is designed for podcast creators, content marketers, and video producers who want to convert YouTube videos into podcast-ready scripts. It's perfect for anyone repurposing long-form content to reach audio-first audiences without manual effort.\n\n---\n\n### 🧠 What problem is this workflow solving?\nCreating podcast scripts from YouTube videos manually is time-consuming. This workflow automates the process by pulling transcripts, cleaning the text, organizing the dialogue, summarizing the key points, and saving everything in one place. It removes the need for manual transcription, formatting, and structuring.\n\n---\n\n### ⚙️ What this workflow does\n\nThis workflow uses **Dumpling AI** and **GPT-4o** to automate the transformation of YouTube video transcripts into polished podcast scripts. Here's how it works:\n\n1. **RSS Feed Trigger**\n - Monitors a YouTube RSS feed for new video uploads. When a new video is detected, the workflow begins automatically.\n\n2. **Get YouTube Transcript (Dumpling AI)**\n - Uses Dumpling AI's `get-youtube-transcript` endpoint to extract the full transcript from the video URL.\n\n3. **Generate Podcast Script with GPT-4o**\n - GPT-4o receives the transcript and generates a structured JSON output including:\n - Cleaned transcript with filler words removed\n - Speaker labels for clarity\n - A short, engaging podcast title\n - A concise summary of the episode\n\n4. **Save to Airtable**\n - The structured data (title, summary, cleaned transcript) is saved to Airtable for easy review, editing, or publishing.\n\n---\n\nThis automation is an ideal workflow for repurposing video content into audio-friendly formats, cutting down production time while increasing content output across platforms.\n", - "nodes": [ - "n8n-nodes-base.rssFeedReadTrigger", - "n8n-nodes-base.httpRequest", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.airtable", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:other", "ai", "integration:httpRequest"], - "triggerType": "other", - "hasAI": true, - "score": 85.15, - "scoreBreakdown": { - "traction": 0.507, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/4887", - "author": "yang", - "success": true - }, - { - "id": 5910, - "slug": "auto-generate-and-post-instagram-reels-with-veo3-openai-and--5910", - "name": "Auto-Generate and Post Instagram Reels with Veo3, OpenAI, and Blotato", - "description": "📱 Veo3 Instagram Agent – Create & Auto-Post Reels with Blotato\n\nDescription:\nThis no-code workflow automates the full pipeline of generating and publishing Instagram Reels using Veo3 (via Wavespeed API). From prompt to post, it handles content ideation, short-form video generation, caption writing, logging, and even automatic publishing to Instagram via Blotato.\n\nPerfect for creators, brands, and marketers who want to scale content creation without needing to shoot or edit videos manually.\n\n🔗 Watch the full step-by-step tutorial on how to build this workflow:\nhttps://youtu.be/s-KzxeKWmIA?si=6x8WKMeiyWodZWVq\n\nGoogle Sheet Template: https://docs.google.com/spreadsheets/d/1bA-PQTrvekC1Rti-XumGANgjIwLjvcFCqoIxVCYsq2E/edit?usp=sharing\n\n🚀 What This Workflow Does:\nTrigger via Chat or Telegram\n Start with a simple message like:\n \"Make a reel for a luxury minimalist candle brand using calm aesthetics.\"\n\nAI Video Prompt Generation\n Uses OpenAI to craft a visually rich, platform-optimized video description prompt.\n\n🎞️ Video Creation with Veo3 API\n Submits your prompt to Veo3 to create a short video (9:16 ratio, 8 seconds) with motion, tone, and trend styles.\n\n✍️ Caption Writing\n An AI agent writes an engaging and playful caption based on the video content.\n\n📄 Google Sheets Logging\n Stores prompt, video URL, caption, and status in a GSheet to keep track of all generated assets.\n\n📤 Auto-Publish to Instagram\n Posts the video + caption directly to Instagram using Blotato’s social media publishing API.\n\n🔌 Tools & Integrations Used:\nOpenAI for prompt & caption generation\n\nWavespeed API (Veo3) for video generation\n\nGoogle Sheets for tracking\n\nBlotato for scheduling & publishing content\n\nn8n for orchestration and automation logic\n\n💡 Use Cases:\nContent calendar automation for small teams\n\nTrend-based ad creation and testing\n\nUGC-style reel generation for e-commerce\n\nRapid ideation & creative experimentation", - "nodes": [ - "@n8n/n8n-nodes-langchain.chatTrigger", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.wait", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.if", - "n8n-nodes-base.set", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:chatTrigger", "ai", "integration:httpRequest"], - "triggerType": "chatTrigger", - "hasAI": true, - "score": 85.1, - "scoreBreakdown": { - "traction": 0.62, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.538 - }, - "source": "https://n8n.io/workflows/5910", - "author": "marconi", - "success": true - }, - { - "id": 5271, - "slug": "learn-n8n-expressions-with-an-interactive-step-by-step-tutor-5271", - "name": "🎓 Learn n8n Expressions with an Interactive Step-by-Step Tutorial for Beginners", - "description": "## How it works\n\nThis template is an interactive, step-by-step tutorial designed to teach you the most important skill in n8n: **using expressions to access and manipulate data**.\n\nIf you know what JSON is but aren't sure how to pull a specific piece of information from one node and use it in another, this workflow is for you. It starts with a single \"Source Data\" node that acts as our filing cabinet, and then walks you through a series of lessons, each demonstrating a new technique for retrieving and transforming that data.\n\nYou will learn how to:\n1. **Access a simple value** from a previous node.\n2. Use n8n's built-in selectors like **`.last()`** and **`.first()`**.\n3. Get a specific item from a list (**Array**).\n4. Drill down into nested data (**Objects**).\n5. Combine these techniques to access data in an **array of objects**.\n6. Go beyond simple retrieval by using **JavaScript functions** to do math or change text.\n7. Inspect data with utility functions like **`Object.keys()`** and **`JSON.stringify()`**.\n8. Summarize data from **multiple items** using `.all()` and arrow functions.\n\n## Set up steps\n\n**Setup time: 0 minutes!**\n\nThis workflow is a self-contained tutorial and requires no setup or external credentials.\n\n1. Click **\"Execute Workflow\"** to run the entire tutorial.\n2. Follow the flow from the \"Source Data\" node to the \"Final Exam\" node.\n3. For each lesson, click on the node to see how its expressions are configured in the parameters panel.\n4. Read the detailed sticky note next to each lesson—it breaks down exactly how the expression works and why.\n\nBy the end, you'll have the foundational knowledge to connect data and build powerful, dynamic workflows in n8n.", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.set", - "n8n-nodes-base.splitOut" - ], - "tags": ["trigger:manual", "integration:set"], - "triggerType": "manual", - "hasAI": false, - "score": 85.09, - "scoreBreakdown": { - "traction": 0.697, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.231 - }, - "source": "https://n8n.io/workflows/5271", - "author": "lucaspeyrin", - "success": true - }, - { - "id": 10427, - "slug": "analyze-facebook-ads-send-insights-to-google-sheets-with-gem-10427", - "name": "Analyze Facebook Ads & Send Insights to Google Sheets with Gemini AI", - "description": "Stop manually digging through Meta Ads data and spending hours trying to connect the dots.\n\nThis workflow turns n8n into an AI-powered media buyer that automatically analyzes your ad performance, categorizes your creatives, and delivers insights directly into a Google Sheet.\n\n➡️ Watch the full 4-part setup and tutorial on YouTube:\nhttps://youtu.be/hxQshcD3e1Y\n\nAbout This 4-Part Automation Series\n\nAs a media buyer, I built this system to automate the heavy lifting of analyzing ad data and brainstorming new creative ideas.\nThis template is the first foundational part of that larger system.\n\n✅ Part 1 (This Template): Pulling Ad Data & Getting Quick Insights\nAutomatically pulls data into a Google Sheet and uses an LLM to categorize ad performance.\n\n✅ Part 2: Finding the Source Files for the Best Ads\nFetches the image or video files for top-performing ads.\n\n✅ Part 3: Using AI to Understand Why an Ad Works\nSends your best ads to Google Gemini for structured notes on hooks, transcripts, and visuals.\n\n✅ Part 4: Getting the AI to Suggest New Creative Ideas\nUses all the insights to generate fresh ad concepts, scripts, and creative briefs.\n\nWhat This Template (Part 1) Does\n\nSecure Token Management\nAutomatically retrieves and refreshes your Facebook long-term access token.\n\nFetch Ad Data\nPulls the last 28 days of ad-level performance data from your Facebook Ads account.\n\nProcess & Clean\nParses raw data, standardizes key e-commerce metrics (like ROAS), and filters for sales-focused campaigns.\n\nBenchmark Calculation\nAggregates all data to create an overall performance benchmark (e.g., average Cost Per Purchase).\n\nAI Analysis\nA “Senior Media Buyer” AI persona evaluates each ad against the benchmark and categorizes it as “HELL YES,” “YES,” or “MAYBE,” with justifications.\n\nOutput to Google Sheets\nUpdates your Google Sheet with both raw performance data and AI-generated insights.\n\nWho Is It For?\n\nE-commerce store owners\n\nDigital marketing agencies\n\nFacebook Ads media buyers\n\nHow to Set It Up\n\nCredentials\n\nConnect your Google Gemini and Google Sheets accounts in the respective nodes.\n\nThe template uses NocoDB for token management. Configure the “Getting Long-Term Token” and “Updating Token” nodes — or replace them with your preferred credential storage method.\n\nUpdate Your IDs\n\nIn the “Getting Data For the Past 28 Days…” HTTP Request node, replace act_XXXXXX in the URL with your Facebook Ad Account ID.\n\nIn both Google Sheets nodes (“Sending Raw Data…” and “Updating Ad Insights…”), update the Document ID with your target Google Sheet’s ID.\n\nRun the Workflow\n\nClick “Test workflow” to run your first AI-powered analysis!\n\nTools Used\n\nn8n\n\nFacebook for Developers\n\nGoogle AI Studio (Gemini)\n\nNocoDB (or any credential database of your choice)", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.code", - "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", - "@n8n/n8n-nodes-langchain.outputParserStructured", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.nocoDb", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.set", - "n8n-nodes-base.sort", - "n8n-nodes-base.filter", - "@n8n/n8n-nodes-langchain.chainLlm", - "n8n-nodes-base.merge", - "n8n-nodes-base.if", - "n8n-nodes-base.aggregate" - ], - "tags": ["trigger:manual", "ai", "integration:code"], - "triggerType": "manual", - "hasAI": true, - "score": 85.08, - "scoreBreakdown": { - "traction": 0.61, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.577 - }, - "source": "https://n8n.io/workflows/10427", - "author": "jj-tham", - "success": true - }, - { - "id": 5396, - "slug": "ai-timesheet-generator-with-gmail-calendar-github-to-google--5396", - "name": "AI Timesheet Generator with Gmail, Calendar & GitHub to Google Sheets", - "description": "# AI-Powered Automatic Timesheet Generator for Google Sheets\n\nStop wasting billable hours on manual time-tracking. **AutoTimesheet Pro** uses AI to collect emails, meetings, and GitHub work, then writes a clean timesheet straight into Google Sheets. Perfect for developers, consultants, agencies, and remote teams.\n\n## Get Started with [n8n](https://n8n.partnerlinks.io/ds9podzjls6d) now!\n\n---\n\n## 🚀 Key Features\n\n- **Automated Google Sheets time-tracking** — zero spreadsheet prep. \n- **AI-generated activity summaries** (≤ 120 chars) via OpenAI GPT-4o-mini. \n- **Gmail integration** — logs only important emails, skipping newsletters & no-replies. \n- **Google Calendar time logger** — captures confirmed events, duration, and attendees. \n- **GitHub commit & PR tracker** — records your commits plus opened/closed PRs. \n- **Daily 7 PM cron trigger** (easily adjustable). \n- **Month-based sheet creation** — new tab spins up on the first run each month. \n- **No-code n8n template** — just connect credentials and tweak one **Set Variables** node.\n- **🔌 Easily extensible** — drag-and-drop extra n8n nodes to add Slack, Jira, Notion, Asana, Trello, Toggl, or any other data source you need.\n\n---\n\n## 🔍 How It Works\n\n1. **Collect** — n8n pulls data from Gmail, Google Calendar, and chosen GitHub repos. \n2. **Clean** — filters remove noise (newsletters, irrelevant commits, etc.). \n3. **Condense** — OpenAI rewrites each item into a concise, SEO-friendly description. \n4. **Write** — workflow appends *Date*, *Type*, and *Description* to your **Timesheet** Google Sheet.\n5. **Extend** — simply insert new n8n nodes (e.g., Slack, Notion, Jira) and merge them into the same pipeline.\n\n---\n\n## 📈 Benefits for SEO-Minded Professionals\n\n- **Keyword-rich activity log** improves internal search and reporting. \n- **Structured data** in Sheets simplifies export to accounting or PM tools. \n- **Consistent naming** (`CALENDAR_EVENT`, `EMAIL`, `COMMIT`, `PR`) makes analytics easy.\n\n---\n\n## ✅ Why Choose AutoTimesheet Pro?\n\n- Zero manual entry — just open the sheet and bill clients. \n- Immediate visibility into where your hours went. \n- Works with any GitHub repo list and any inbox you own. \n- 100 % no-code setup — activate in minutes.\n- Built on n8n, so you can customize and scale without limits.\n---\n\n## 📥 Get Started\n\nReady to replace manual time-tracking with smart automation? \n\n#### https://n8n.partnerlinks.io/ds9podzjls6d\n\nJoin **N8N** now, connect your Google & GitHub accounts, and let AI handle your daily log.\n\n---", - "nodes": [ - "n8n-nodes-base.googleCalendar", - "n8n-nodes-base.gmail", - "n8n-nodes-base.github", - "n8n-nodes-base.code", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.aggregate", - "n8n-nodes-base.merge", - "n8n-nodes-base.cron", - "n8n-nodes-base.set", - "n8n-nodes-base.if", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.googleSheets" - ], - "tags": ["trigger:schedule", "ai", "integration:aggregate"], - "triggerType": "schedule", - "hasAI": true, - "score": 85.07, - "scoreBreakdown": { - "traction": 0.618, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.542 - }, - "source": "https://n8n.io/workflows/5396", - "author": "zivkovic58", - "success": true - }, - { - "id": 13904, - "slug": "auto-create-instagram-carousel-posts-from-canva-with-openai--13904", - "name": "Auto-create Instagram carousel posts from Canva with OpenAI and Blotato", - "description": "# Auto-Create Instagram Carousel Posts from Canva (Webhook → AI Caption → Instagram)\n\nAutomatically turn Canva carousel exports into a fully published Instagram post with AI-generated captions.\nThis workflow receives a webhook payload containing PNG slides (for example from Claude Cowork or a Canva automation), uploads the images, generates an optimized Instagram caption using AI, and publishes the carousel post via Blotato.\nIt removes the manual work of downloading slides, writing captions, and posting to Instagram.\n\n## What this template does\nWhen triggered by a webhook, this workflow:\nReceives a payload containing carousel slide images\nSplits the slides into individual items\nUploads each image to a media host\nAggregates all images into a carousel\nUses AI to generate an Instagram caption with hashtags\nPublishes the post to Instagram via Blotato\nThis allows you to go from Canva design → Instagram post automatically.\n\n## Example use cases\nThis workflow is useful for:\nSocial media managers automating Instagram posting\nContent creators producing carousel posts at scale\nAgencies generating client posts automatically\nAI-assisted content pipelines\nMarketing automation systems\nExample automation pipeline:\nClaude → Canva → n8n → Instagram\n\nWorkflow overview\nWebhook → Split Slides → Upload Media → Aggregate Images → Generate Caption → Publish Instagram Post\nNode breakdown\nWebhook\nReceives a POST request containing the carousel slide data.\nThe payload includes slide metadata and PNG URLs generated by Canva.\nSplit Out Slides - Splits the slides array so each slide is processed individually.\nThis allows each PNG image to be uploaded and processed as its own item.\nUpload Media (Blotato) - Uploads each slide image to the media host used by the social publishing platform.\nAggregate collects all uploaded image URLs into a single list.\nMessage a Model (AI Caption Generator)\nTelegram Node for Approval\nCreate Post (Blotato) - Creates and publishes the Instagram carousel post\n\n## Requirements\nBefore using this template you must configure the following:\nOpenAI API credential\nBlotato API credential\nInstagram account connected to Blotato\nA system that sends the webhook (Claude, Canva, or another automation)\n\n## Setup instructions\nImport the workflow into n8n\nConfigure credentials:\nOpenAI\nBlotato\nCopy the webhook URL from the Webhook node\nConfigure your upstream tool (Claude / Canva / automation) to send slide data to this webhook\nTest the workflow by sending a sample payload\nActivate the workflow\nOnce active, new carousel payloads will automatically be turned into Instagram posts\n\n## Customization ideas\nYou can easily extend this workflow by:\nAdding Telegram approval before posting\nPosting to TikTok, LinkedIn, or Facebook\nStoring posts in Google Sheets or Notion\nGenerating captions in different tones or languages\nAdding AI analysis of the slides to generate smarter captions", - "nodes": [ - "n8n-nodes-base.webhook", - "n8n-nodes-base.splitOut", - "@blotato/n8n-nodes-blotato.blotato", - "n8n-nodes-base.aggregate", - "n8n-nodes-base.telegram", - "n8n-nodes-base.if", - "n8n-nodes-base.noOp", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:webhook", "ai", "integration:blotato"], - "triggerType": "webhook", - "hasAI": true, - "score": 85.04, - "scoreBreakdown": { - "traction": 0.552, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.8 - }, - "source": "https://n8n.io/workflows/13904", - "author": "marconi", - "success": true - }, - { - "id": 10174, - "slug": "ppc-campaign-intelligence-optimization-with-google-ads-sheet-10174", - "name": "PPC Campaign Intelligence & Optimization with Google Ads, Sheets & Slack", - "description": "## How it Works\n\nThis workflow automatically monitors your Google Ads campaigns every day, analyzing performance with AI-powered scoring to identify scaling opportunities and catch issues before they drain your budget. Each morning at 9 AM, it fetches all active campaign data including clicks, impressions, conversions, costs, and conversion rates from your Google Ads account.\n\nThe AI analysis engine evaluates four critical dimensions: CTR (click-through rate) to measure ad relevance, conversion rate to assess landing page effectiveness, cost per conversion to evaluate profitability, and traffic volume to identify scale-readiness. Each campaign receives a performance score (0-100 points) and is automatically categorized as Excellent (75+), Good (55-74), Fair (35-54), or Underperforming (0-34).\n\nHigh-performing campaigns trigger instant Slack alerts to your PPC team with detailed scaling recommendations and projected ROI improvements, while underperforming campaigns generate urgent alerts with specific optimization actions. Every campaign is logged to your Google Sheets dashboard with daily metrics, and the system generates personalized email reports—action-oriented scaling plans for top performers and troubleshooting guides for campaigns needing attention.\n\nThe entire analysis takes minutes, providing your team with daily intelligence reports that would otherwise require hours of manual spreadsheet work and data analysis.\n\n---\n\n## Who is this for?\n\n- PPC managers and paid media specialists drowning in campaign data and manual reporting\n- Marketing agencies managing multiple client accounts needing automated performance monitoring\n- E-commerce brands running high-spend campaigns who can't afford budget waste\n- Growth teams looking to scale winners faster and pause losers immediately\n- Anyone spending $5K+ monthly on Google Ads who needs data-driven optimization decisions\n\n---\n\n## Setup Steps\n\n- **Setup time:** Approx. 15-25 minutes (credential configuration, dashboard setup, alert customization)\n- **Requirements:**\n - Google Ads account with active campaigns\n - Google account with a tracking spreadsheet\n - Slack workspace\n - SMTP email provider (Gmail, SendGrid, etc.)\n\n- Create a Google Sheets dashboard with two tabs: \"Daily Performance\" and \"Campaign Log\" with appropriate column headers.\n- Set up these nodes:\n - **Schedule Daily Check:** Pre-configured to run at 9 AM daily (adjust timing if needed).\n - **Fetch Google Ads Data:** Connect your Google Ads account and authorize API access.\n - **AI Performance Analysis:** Review scoring thresholds (CTR, conversion rate, cost benchmarks).\n - **Route by Performance:** Automatically splits campaigns into high-performers vs. issues.\n - **Update Campaign Dashboard:** Connect Google Sheets and select your \"Daily Performance\" tab.\n - **Log All Campaigns:** Select your \"Campaign Log\" tab for historical tracking.\n - **Slack Alerts:** Connect workspace and configure separate channels for scaling opportunities and performance issues.\n - **Generate Action Plan:** Customize email templates with your brand voice and action items.\n - **Email Performance Report:** Configure SMTP and set recipient email addresses.\n\n- Credentials must be entered into their respective nodes for successful execution.\n\n---\n\n## Customization Guidance\n\n- **Scoring Weights:** Adjust point values for CTR (30), conversion rate (35), cost efficiency (25), and volume (10) in the AI Performance Analysis node based on your business priorities.\n- **Performance Thresholds:** Modify the 75-point Excellent threshold and 55-point Good threshold to match your campaign quality distribution and industry benchmarks.\n- **Benchmark Values:** Update CTR benchmarks (5% excellent, 3% good, 1.5% average) and conversion rate targets (10%, 5%, 2%) for your industry.\n- **Alert Channels:** Create separate Slack channels for different alert types or route critical alerts to Microsoft Teams, Discord, or SMS via Twilio.\n- **Email Recipients:** Configure different recipient lists for scaling alerts (executives, growth team) vs. optimization alerts (campaign managers).\n- **Schedule Frequency:** Change from daily to hourly monitoring for high-spend campaigns, or weekly for smaller accounts.\n- **Additional Platforms:** Duplicate the workflow structure for Facebook Ads, Microsoft Ads, or LinkedIn Ads with platform-specific nodes.\n- **Budget Controls:** Add nodes to automatically pause campaigns exceeding cost thresholds or adjust bids based on performance scores.\n\n---\n\nOnce configured, this workflow will continuously monitor your ad spend, identify opportunities worth thousands in additional revenue, and alert you to issues before they waste your budget—transforming manual reporting into automated intelligence.\n\n---\n\n**Built by Daniel Shashko** \n[Connect on LinkedIn](https://www.linkedin.com/in/daniel-shashko/)", - "nodes": [ - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.googleAds", - "n8n-nodes-base.code", - "n8n-nodes-base.if", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.slack", - "n8n-nodes-base.emailSend", - "n8n-nodes-base.set", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:schedule", "integration:code"], - "triggerType": "schedule", - "hasAI": false, - "score": 85.03, - "scoreBreakdown": { - "traction": 0.57, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.727 - }, - "source": "https://n8n.io/workflows/10174", - "author": "tomax", - "success": true - }, - { - "id": 5149, - "slug": "create-pdf-from-html-with-gotenberg-5149", - "name": "🛠️ Create PDF from HTML with Gotenberg", - "description": "## How it works\n\nThis workflow converts an HTML string into a polished PDF file using the powerful open-source [Gotenberg](https://gotenberg.dev/) service. It's designed to be a reusable utility in your automation stack.\n\n1. **Receives Input:** The workflow is triggered with a JSON object containing the full `html` code as a string and a desired `file_name` for the output.\n2. **Prepares File:** It converts the incoming HTML string into a binary `index.html` file, which is required for the API call.\n3. **Calls Gotenberg API:** It sends the HTML file to a running Gotenberg instance via an HTTP request. It also dynamically sets the output filename and embeds metadata (like Author, Title, and Creation Date) directly into the PDF.\n4. **Returns PDF:** The workflow outputs the final binary PDF file, ready to be saved, sent in an email, or used in the next step of your main workflow.\n\n## Set up steps\n\n**Setup time: ~3 minutes**\n\nThis workflow has one critical prerequisite: a running Gotenberg instance that your n8n can connect to.\n\n**1. Prerequisite: Run Gotenberg**\n\nYou need to have the Gotenberg service running. The easiest way is with Docker. Add the following service to your `docker-compose.yml` file (the same one you use for n8n):\n\n```yaml\nservices:\n # ... your n8n service ...\n\n gotenberg:\n image: gotenberg/gotenberg:8\n restart: always\n```\n\nThen, restart your stack with `docker compose up -d`. This makes Gotenberg available at the address `http://gotenberg:3000` from within your n8n container.\n\n**2. Use as a Sub-Workflow**\n\nThis workflow is ready to be used as a sub-workflow.\n\n1. In your main workflow, add an **Execute Sub-Workflow** node.\n2. In the **Workflow** parameter, select this \"Create PDF from HTML\" workflow.\n3. Provide the input data in the required format: a JSON object with `html` and `file_name` keys.", - "nodes": [ - "n8n-nodes-base.convertToFile", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.executeWorkflowTrigger", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:other", "integration:convertToFile"], - "triggerType": "other", - "hasAI": false, - "score": 84.99, - "scoreBreakdown": { - "traction": 0.499, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/5149", - "author": "lucaspeyrin", - "success": true - }, - { - "id": 5218, - "slug": "find-the-cheapest-flights-automatically-with-bright-data-n8n-5218", - "name": "Find the Cheapest Flights Automatically with Bright Data & n8n", - "description": "# \n\n## Description\n\nThis workflow automatically searches multiple flight booking websites to find the cheapest flights for your desired routes. It leverages web scraping to compare prices across platforms, helping you save money on air travel.\n\n## Overview\n\nThis workflow automatically searches multiple flight booking websites to find the cheapest flights for your desired routes. It uses Bright Data to scrape flight prices and can notify you when prices drop below your target threshold.\n\n### Tools Used\n\n* **n8n:** The automation platform that orchestrates the workflow.\n* **Bright Data:** For scraping flight prices from booking websites.\n* **Notification Services:** Email, SMS, or other messaging platforms.\n\n## How to Install\n\n1. **Import the Workflow:** Download the `.json` file and import it into your n8n instance.\n2. **Configure Bright Data:** Add your Bright Data credentials to the Bright Data node.\n3. **Set Up Notifications:** Configure your preferred notification method.\n4. **Customize:** Set your routes, date ranges, and price thresholds.\n\n## Use Cases\n\n* **Frequent Travelers:** Find the best deals for your regular routes.\n* **Travel Agencies:** Monitor flight prices for client bookings.\n* **Budget Travelers:** Get notified when flights to your dream destination become affordable.\n\n---\n\n## Connect with Me\n\n* **YouTube:** [https://www.youtube.com/@YaronBeen/videos](https://www.youtube.com/@YaronBeen/videos)\n* **LinkedIn:** [https://www.linkedin.com/in/yaronbeen/](https://www.linkedin.com/in/yaronbeen/)\n* **Get Bright Data:** [https://get.brightdata.com/1tndi4600b25](https://get.brightdata.com/1tndi4600b25) (Using this link supports my free workflows with a small commission)\n\n#n8n #automation #travel #flights #brightdata #dealalerts #webscraping #flightdeals #cheapflights #travelhacks #budgettravel #travelplanning #airfare #flightprices #travelautomation #n8nworkflow #workflow #nocode #traveltech #flightbooking #savemoney #traveltools #flightcomparison #bestflightdeals #travelsmarter #automatedtravel\n", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.html", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.httpRequest" - ], - "tags": ["trigger:manual", "integration:html"], - "triggerType": "manual", - "hasAI": false, - "score": 84.98, - "scoreBreakdown": { - "traction": 0.499, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/5218", - "author": "yaron-nofluff", - "success": true - }, - { - "id": 3053, - "slug": "technical-stock-analysis-with-telegram-airtable-and-a-gpt-po-3053", - "name": "Technical stock analysis with Telegram, Airtable and a GPT-powered AI Agent", - "description": "### Video Guide\n\nI prepared a detailed guide that demonstrates the complete process of building a trading agent automation using n8n and Telegram, seamlessly integrating various functions for stock analysis.\n\n[![Youtube Thumbnail](https://res.cloudinary.com/de9jgixzm/image/upload/v1740845597/Youtube%20Thumbs/Video%2022%20-%20Philipp%20Trading%20Blur.png)](https://youtu.be/94vh6hSiP9s)\n\n[Youtube Link](https://youtu.be/94vh6hSiP9s)\n\n### Who is this for?\nThis workflow is perfect for traders, financial analysts, and developers looking to automate stock analysis interactions via Telegram. It’s especially valuable for those who want to leverage AI tools for technical analysis without needing to write complex code.\n\n### What problem does this workflow solve?\nMany traders desire real-time analysis of stock data but lack the technical expertise or tools to perform in-depth analysis. This workflow allows users to easily interact with an AI trading agent through Telegram for seamless stock analysis, chart generation, and technical evaluation, all while eliminating the need for manual interventions.\n\n### What this workflow does\nThis workflow utilizes n8n to construct an end-to-end automation process for stock analysis through Telegram communication. The setup involves:\n- Receiving messages via a Telegram bot.\n- Processing audio or text messages for trading queries.\n- Transcribing audio using OpenAI API for interpretation.\n- Gathering and displaying charts based on user-specified parameters.\n- Performing technical analysis on generated charts.\n- Sending back the analyzed results through Telegram.\n\n\n### Setup\n\n1. **Prepare Airtable**:\n - Create simple table to store tickers.\n\n2. **Prepare Telegram Bot**:\n - Ensure your Telegram bot is set up correctly and listening for new messages.\n\n3. **Replace Credentials**:\n - Update all nodes with the correct credentials and API keys for services involved.\n\n4. **Configure API Endpoints**:\n - Ensure chart service URLs are correctly set to interact with the corresponding APIs properly.\n\n5. **Start Interaction**:\n - Message your bot to initiate analysis; specify ticker symbols and desired chart styles as required.", - "nodes": [ - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "@n8n/n8n-nodes-langchain.toolWorkflow", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.telegramTrigger", - "n8n-nodes-base.telegram", - "n8n-nodes-base.executeWorkflowTrigger", - "n8n-nodes-base.set", - "n8n-nodes-base.httpRequest", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.switch", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.webhook", - "n8n-nodes-base.airtableTool", - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.airtable", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:telegram", "ai", "integration:langchain"], - "triggerType": "telegram", - "hasAI": true, - "score": 84.96, - "scoreBreakdown": { - "traction": 0.581, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.667 - }, - "source": "https://n8n.io/workflows/3053", - "author": "lowcodingdev", - "success": true - }, - { - "id": 4336, - "slug": "webhook-enabled-ai-pdf-analyzer-4336", - "name": "Webhook-enabled AI PDF analyzer", - "description": "## Who this template is for\n\nThis template is for researchers, students, professionals, or content creators who need to quickly extract and summarize key insights from PDF documents using AI-powered analysis.\n\n## Use case\n\nConverting lengthy PDF documents into structured, digestible summaries organized by topic with key insights. This is particularly useful for processing research papers, reports, whitepapers, or any document where you need to quickly understand the main topics and extract actionable insights without reading the entire document.\n\n## How this workflow works\n\n1. **Document Upload**: Receives PDF files through a POST endpoint at `/ai_pdf_summariser`\n2. **File Validation**: Checks that the PDF is under 10MB and has fewer than 20 pages to meet API limits\n3. **Content Extraction**: Extracts text content from the PDF file\n4. **AI Analysis**: Uses OpenAI's GPT-4o-mini to analyze the document and break it down into distinct topics\n5. **Insight Generation**: For each topic, generates 3 key insights with titles and detailed explanations\n6. **Format Response**: Converts the structured data into markdown format for easy reading\n7. **Return Results**: Provides the formatted summary along with document metadata (file hash)\n\n## Set up steps\n\n1. **Configure OpenAI API**: Set up your OpenAI credentials for the GPT-4o-mini model\n2. **Deploy Webhook**: The workflow automatically creates a POST endpoint at `/ai_pdf_summariser`\n3. **Test Upload**: Send a PDF file to the endpoint using a multipart/form-data request\n4. **Adjust Limits**: Modify the file size (10MB) and page count (20) validation limits if needed based on your requirements\n5. **Customize Prompts**: Update the system prompt in the Information Extractor node to change how topics and insights are generated\n\nThe workflow includes comprehensive error handling for file validation failures (400 error) and processing errors (500 error), ensuring reliable operation even with problematic documents.", - "nodes": [ - "n8n-nodes-base.noOp", - "n8n-nodes-base.respondToWebhook", - "n8n-nodes-base.if", - "n8n-nodes-base.code", - "n8n-nodes-base.extractFromFile", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.informationExtractor", - "n8n-nodes-base.set", - "n8n-nodes-base.crypto", - "n8n-nodes-base.webhook", - "@n8n/n8n-nodes-langchain.lmChatOpenAi" - ], - "tags": ["trigger:webhook", "ai", "integration:respondToWebhook"], - "triggerType": "webhook", - "hasAI": true, - "score": 84.94, - "scoreBreakdown": { - "traction": 0.539, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.833 - }, - "source": "https://n8n.io/workflows/4336", - "author": "n8n-team", - "success": true - }, - { - "id": 3666, - "slug": "real-estate-lead-generation-with-batchdata-skip-tracing-crm--3666", - "name": "Real Estate Lead Generation with BatchData Skip Tracing & CRM Integration", - "description": "How It Works\n- \nThis workflow automates the entire property lead generation process in a few simple steps:\n\n1. Property Search: Connects to BatchData's Property Search API with customizable parameters (location, property type, value range, equity percentage, etc.)\n2. Lead Filtering & Scoring: Processes results to identify the most promising leads based on criteria like absentee ownership, years owned, equity percentage, and tax status. Each property receives a lead score to prioritize follow-up.\n3. Skip Tracing: Automatically retrieves owner contact information (phone, email, mailing address) for each qualified property.\n4. Data Formatting: Structures all property and owner data into a clean, organized format ready for your systems.\n5. Multi-Channel Output:\n- Generates an Excel spreadsheet with all lead details\n- Pushes leads directly to your CRM (configurable for HubSpot, Salesforce, etc.)\n- Sends a summary email with the spreadsheet attached\n\n\n\nThe workflow can run on a daily schedule or be triggered manually as needed. All parameters are easily configurable through dedicated nodes, requiring no coding knowledge.\n\nWho's It For\n-\nThis workflow is perfect for:\n\n- Real Estate Investors looking to find off-market properties with motivated sellers\n- Real Estate Agents who want to generate listing leads from distressed or high-equity properties\n- Investment Companies that need regular lead flow for acquisitions\n- Real Estate Marketers who run targeted campaigns to property owners\n- Wholesalers seeking to build a pipeline of potential deals\n- Property Service Providers (roof repair, renovation contractors, etc.) who target specific property types\n\nAnyone who needs reliable, consistent lead generation for real estate without the manual work of searching, filtering, and organizing property data will benefit from this automation.\n\nAbout BatchData\n-\nBatchData is a comprehensive property data provider that offers access to nationwide property information, owner details, and skip tracing services. Key features include:\n\n- Extensive Database: Covers 150+ million properties across all 50 states\n- Rich Property Data: Includes ownership information, tax records, sales history, valuation estimates, equity positions, and more\n- Skip Tracing Services: Provides owner contact information including phone numbers, email addresses, and mailing addresses\n- Distressed Property Indicators: Flags for pre-foreclosure, tax delinquency, vacancy, and other motivation factors\n- RESTful API: Professional API for programmatic access to all property data services\n- Regular Updates: Continuously refreshed data for accurate information\n\nBatchData's services are designed for real estate professionals who need reliable property and owner information to power their marketing and acquisition strategies. Their API-first approach makes it ideal for workflow automation tools like N8N.", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.set", - "n8n-nodes-base.code", - "n8n-nodes-base.spreadsheetFile", - "n8n-nodes-base.hubspot", - "n8n-nodes-base.emailSend", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.filter", - "n8n-nodes-base.splitOut" - ], - "tags": ["trigger:manual", "integration:httpRequest"], - "triggerType": "manual", - "hasAI": false, - "score": 84.92, - "scoreBreakdown": { - "traction": 0.538, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.833 - }, - "source": "https://n8n.io/workflows/3666", - "author": "zellerhaus", - "success": true - }, - { - "id": 4485, - "slug": "telegram-ai-chatbot-agent-with-infranodus-graphrag-knowledge-4485", - "name": "Telegram AI Chatbot Agent with InfraNodus GraphRAG Knowledge Base", - "description": "## Using the knowledge graphs instead of RAG vector stores\n\nThis workflow creates a Telegram chatbot agent that has access to several knowledge bases at the same time (used as \"experts\"). \n\nThese knowledge bases are provided using the [InfraNodus GraphRAG](https://infranodus.com/use-case/ai-knowledge-graphs) using the knowledge graphs and providing high-quality responses without the need to set up complex RAG vector store workflows.\n\nThe advantages of using GraphRAG instead of the standard vector stores for knowledge are:\n\n- Easy and quick to set up and update (no complex data import workflows or vector stores needed)\n- A knowledge graph has a holistic view of your knowledge base and knows what it's about\n- Better retrieval of relations between the document chunks = higher quality responses\n\n![InfraNodus knowledge graph](https://support.noduslabs.com/hc/article_attachments/20266752795292)\n\n\n## How it works\nThis template uses the n8n AI agent node as an orchestrating agent that decides which tool (knowledge graph) to use based on the user's prompt. \n\nHere's a description step by step:\n\n- The user submits a question using the Telegram bot, which is then received in the n8n workflow via the Telegram trigger node.\n- The AI agent node checks a list of tools it has access to. Each tool has a description of the knowledge it has auto-generated by InfraNodus. \n- The AI agent decides which tool should be used to generate a response. It may reformulate user's query to be more suitable for the expert. \n- The query is then sent to the official InfraNodus Graph RAG Node or directly to the InfraNodus API via the HTTP node endpoint, which will query the graph that corresponds to that expert. \n- Each InfraNodus GraphRAG expert provides a rich response that takes the whole context into account and provides a response from each expert (graph) along with a list of relevant statements retrieved using a combination or RAG and GraphRAG. \n- The n8n AI Agent node integrates the responses received from the experts to produce the final answer.\n- The final answer is sent back to the Telegram bot who delivers it back to the private chat or a Telegram group.\n\n## How to use\n\nYou need an [InfraNodus GraphRAG API account and key](https://infranodus.com/use-case/ai-knowledge-graphs) to use this workflow. \n\n- Create an InfraNodus account\n- Get the API key at [https://infranodus.com/api-access](https://infranodus.com/api-access) and create a Bearer authorization key for the InfraNodus HTTP nodes.\n- Create a separate knowledge graph for each expert (using PDF / content import options) in InfraNodus\n- For each graph, go to the workflow, paste the name of the graph into the `body` `name` field.\n- Keep other settings intact or learn more about them at the [InfraNodus access points](https://support.noduslabs.com/hc/en-us/articles/13605983537692-InfraNodus-API-Access-Points) page. \n- Once you add one or more graphs as experts to your flow, add the LLM key to the OpenAI node\n- Create a Telegram bot (the instructions are in the workflow Post note) — it takes 30 seconds. Get its API key and create the Telegram credentials to use in the Telegram nodes in this workflow.\n\n## Requirements\n\n- An [InfraNodus](https://infranodus.com/use-case/ai-knowledge-graphs) account and API key\n- An OpenAI (or any other LLM) API key\n- A Telegram account\n\n\n## Customizing this workflow\n\nYou can use this same workflow with a standard AI chatbot via a URL that can also be embedded to any website. You can also use it with [ElevenLabs AI voice agent](https://support.noduslabs.com/hc/en-us/articles/20318967066396-How-to-Build-a-Text-Voice-AI-Agent-Chatbot-with-n8n-Elevenlabs-and-InfraNodus). There are many more customizations available. \n\nCheck out the **complete guide** at [https://support.noduslabs.com/hc/en-us/articles/20174217658396-Using-InfraNodus-Knowledge-Graphs-as-Experts-for-AI-Chatbot-Agents-in-n8n](https://support.noduslabs.com/hc/en-us/articles/20174217658396-Using-InfraNodus-Knowledge-Graphs-as-Experts-for-AI-Chatbot-Agents-in-n8n)\n\nAlso check out the **video tutorial** with a demo:\n\n[![Video tutorial](https://img.youtube.com/vi/kS0QTUvcH6E/sddefault.jpg)](https://www.youtube.com/watch?v=kS0QTUvcH6E)\n\n## Support\n\nIf you have any questions, contact us via the support portal at [https://support.noduslabs.com](https://support.noduslabs.com) or via our [Discord channel](https://discord.gg/v4BWAvTfB9).\n\nMore n8n workflows are available on our support portal: [n8n x InfraNodus AI automation workflows](https://support.noduslabs.com/hc/en-us/sections/18343587412252-AI-RAG-GraphRAG-and-LLM-Workflows).\n\n\n\n", - "nodes": [ - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "n8n-nodes-base.telegramTrigger", - "n8n-nodes-base.telegram", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.httpRequestTool", - "n8n-nodes-infranodus.infranodusTool" - ], - "tags": ["trigger:telegram", "ai", "integration:httpRequestTool"], - "triggerType": "telegram", - "hasAI": true, - "score": 84.92, - "scoreBreakdown": { - "traction": 0.6, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.583 - }, - "source": "https://n8n.io/workflows/4485", - "author": "infranodus", - "success": true - }, - { - "id": 5926, - "slug": "get-a-summary-of-each-podcast-in-your-youtube-playlist-daily-5926", - "name": "Get a summary of each podcast in your YouTube playlist daily automatically free", - "description": "![youtube.png](fileId:2183)\n\n\n## Understand the workflow better.\n[watch this video](https://www.youtube.com/@ARRE-automation)\n## Good to know:\n\nThis workflow automatically transcribes your favorite podcasts or videos saved in a YouTube playlist and generates a comprehensive, AI-powered summary—so you can quickly understand the main topics and insights without having to watch or listen to the entire episode.\n\n## 👤 Who is this for?\n\nPodcast fans who want to save time and get the key points from episodes\n\nBusy professionals who follow educational or industry videos and need quick takeaways\n\nContent creators or researchers who organize and review large amounts of video/audio material\n\nAnyone who wants to efficiently capture and summarize information from YouTube playlists\n\n## ❓ What problem is this workflow solving?\n\nThis workflow solves the challenge of information overload from long-form podcasts and videos. It:\n\nAutomatically transcribes each video or podcast episode in your chosen YouTube playlist\n\nUses AI to create a clear, well-structured summary of the content\n\nLets you learn and extract valuable information without watching or listening to the entire recording\n\nOrganizes everything in a Google Sheets document for easy tracking and future reference\n\n## ✅ What this workflow does:\n\n📺 Fetches all videos from a specified YouTube playlist\n\n🔗 Extracts video titles, URLs, and IDs\n\n📝 Retrieves and combines transcripts for each video or podcast episode\n\n📜 Processes transcript data for clarity\n\n🤖 Uses AI to generate a detailed, sectioned summary that covers all main topics and insights\n\n📊 Automatically logs video titles, transcripts, summaries, and row numbers to a Google Sheets spreadsheet\n\n## ⚙️ How it works:\n\n🟢 Trigger: Start the workflow manually or on a schedule\n\n📺 Fetch videos from your chosen YouTube playlist\n\n🔗 Extract and organize video details (title, URL, ID)\n\n📝 Retrieve the transcript for each video or podcast episode\n\n📜 Combine transcript segments into a single script\n\n✂️ Extract the first sentences for focused summarization\n\n🤖 AI agent creates a comprehensive summary of the episode or video\n\n📊 Save all data—title, transcript, summary, and row number—to Google Sheets\n\n## 🛠️ How to use:\n\nSet up YouTube OAuth2 credentials in n8n\n\nConfigure Google Sheets OAuth2 credentials\n\nSet up API credentials for transcript and AI processing\n\nCreate and link your Google Sheets document\n\nInput your playlist ID and adjust any filters as needed\n\nActivate the workflow\n\n## 📝 Requirements:\n\nn8n instance (cloud or self-hosted)\nYouTube account with OAuth2 access\nGoogle Sheets account\nAccess to transcript and AI APIs\nBasic n8n workflow knowledge\n\n## 🟢 Customizing this workflow:\n\nChange the YouTube playlist ID to target your preferred podcasts or video series\n\nAdjust the transcript retrieval process for other APIs or formats\n\nCustomize the AI prompt for different summary styles or focus areas\n\nAdd or remove fields in the Google Sheets output\n\nChange the workflow trigger or polling frequency\n\nSwitch to a different AI model if desired\n\nThis workflow is designed to help you quickly learn from podcasts and videos you care about—without spending hours consuming the full content.\n", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.youTube", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.googleSheets", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.splitInBatches", - "@n8n/n8n-nodes-langchain.lmChatGroq", - "n8n-nodes-base.code", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.function", - "n8n-nodes-base.splitOut" - ], - "tags": ["trigger:manual", "ai", "integration:code"], - "triggerType": "manual", - "hasAI": true, - "score": 84.82, - "scoreBreakdown": { - "traction": 0.533, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.833 - }, - "source": "https://n8n.io/workflows/5926", - "author": "arre", - "success": true - }, - { - "id": 4151, - "slug": "ai-seo-readability-audit-check-website-friendliness-for-llms-4151", - "name": "AI SEO Readability Audit: Check Website Friendliness for LLMs", - "description": "## Who is this for?\nThis workflow is designed for SEO specialists, content creators, marketers, and website developers who want to ensure their web content is easily accessible, understandable, and indexable by Large Language Models (LLMs) like ChatGPT, Perplexity, and Google AI Overviews. If you're looking to optimize your site for the evolving AI-driven search landscape, this template is for you.\n\n## What problem is this workflow solving? / Use case\nModern AI tools often crawl websites without executing JavaScript. This can lead to them \"seeing\" a very different version of your page than a human user or traditional search engine bot might. This workflow helps you:\n- Identify how much of your content is visible without JavaScript.\n- Check for crucial on-page SEO elements that AI relies on (headings, meta descriptions, structured data).\n- Detect if your site presents JavaScript-blocking warnings.\n- Get an AI-generated readability score and actionable recommendations to improve AI-friendliness.\n\n## What this workflow does\n1. **Receives a URL** via a chat interface.\n2. **Sanitizes the input URL** to ensure it's correctly formatted.\n3. **Fetches the website's HTML** content, simulating a non-JavaScript crawler (like Googlebot).\n4. **Extracts key HTML features**: visible text length, presence of H1/H2/H3 tags, meta description, Open Graph data, structured data (JSON-LD), and `<noscript>` tags. It also checks for common JavaScript-blocking messages.\n5. **Performs an AI SEO Analysis** using an LLM (via OpenAI) based on the extracted features.\n6. **Provides a report** including an AI Readability Score (0-10), a summary, actionable recommendations, and a reminder to check the `robots.txt` file for AI bot access.\n\n## Setup\n- **Estimated setup time:** 2-5 minutes.\n1. Import this workflow into your n8n instance.\n2. Ensure you have an OpenAI account and API key.\n3. Configure the \"OpenAI Chat Model\" node with your OpenAI API credentials. If you don't have credentials set up yet, create new ones in n8n.\n4. Activate the workflow.\n5. Interact with the chat interface provided by the \"When chat message received\" trigger node (you can access this via its webhook URL).\n\n## How to customize this workflow to your needs\n- **Change LLM Model:** In the \"OpenAI Chat Model\" node, you can select a different model that suits your needs or budget.\n- **Adjust AI Prompt:** Modify the prompt in the \"AI SEO Analysis\" node (Chain Llm) to change the focus of the analysis or the format of the report. For example, you could ask for more technical details or a different scoring system.\n- **User-Agent:** The \"Get HTML from Website\" node uses a Googlebot User-Agent. You can change this to simulate other bots if needed.\n- **JS Block Indicators:** The \"Extract HTML Features\" node contains a list of common JavaScript-blocking phrases. You can expand this list with other languages or specific messages relevant to your checks.", - "nodes": [ - "@n8n/n8n-nodes-langchain.chatTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.code", - "@n8n/n8n-nodes-langchain.chainLlm", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.lmChatOpenAi" - ], - "tags": ["trigger:chatTrigger", "ai", "integration:code"], - "triggerType": "chatTrigger", - "hasAI": true, - "score": 84.69, - "scoreBreakdown": { - "traction": 0.526, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.833 - }, - "source": "https://n8n.io/workflows/4151", - "author": "leonardvanhemert", - "success": true - }, - { - "id": 4949, - "slug": "automate-whatsapp-booking-system-with-gpt-4-assistant-cal-co-4949", - "name": "Automate WhatsApp Booking System with GPT-4 Assistant, Cal.com and SMS Reminders", - "description": "\n![Workflow Screenshot](https://www.dr-firas.com/Automate-WhatsApp.png)\n\n# AI-powered WhatsApp booking system with instant SMS confirmations\n\n### Who is this for?\n\nThis workflow is designed for solo entrepreneurs, consultants, coaches, clinics, or any business that handles client appointments and wants to automate the entire scheduling experience via WhatsApp — without the need for live agents.\n\n### What problem is this workflow solving?\n\nResponding to inbound messages, collecting booking details, suggesting available times, and sending reminders can be a huge time drain. This workflow eliminates manual handling by:\n- Automating WhatsApp conversations with an AI assistant\n- Booking appointments directly into Cal.com\n- Sending timely SMS reminders before appointments\n\nIt ensures you never miss a lead or a follow-up — even while you sleep.\n\n### What this workflow does\n\nFrom a single WhatsApp message, the workflow:\n\n1. Triggers via a WhatsApp webhook\n2. Uses GPT-4 to handle conversation flow and qualify the prospect\n3. Collects name, email, selected service\n4. Calls Cal.com API to fetch available time slots\n5. Books the appointment and stores it in Google Sheets\n6. Sends a confirmation message via WhatsApp\n7. Periodically scans for upcoming appointments\n8. Sends SMS reminders to clients 2 hours before their session\n\n### Setup\n\n1. Connect your **Webhook** node to a WhatsApp API (e.g., 360dialog, Twilio, or Ultramsg)\n2. Add your **OpenAI API key** for the GPT-4 nodes\n3. Configure your **Cal.com API key** and set your calendar ID\n4. Link your **Google Sheets** with fields like: `name`, `email`, `date`, `time`, `status`, `reminder_sent`\n5. Connect your **SMS service** (e.g., sms77) with API credentials\n6. Adjust the schedule in the reminder node as needed\n\n### How to customize this workflow to your needs\n\n- **Change the language or tone of the AI assistant** by editing the system prompt in the GPT node\n- **Filter available time slots** by service, team member, or duration\n- **Modify the reminder timing** (e.g., 1 hour before, 24h before, etc.)\n- **Add conditional logic** to route users to different booking flows based on their responses\n- **Integrate additional CRMs** or notification channels like email or Slack\n\n📄 **Documentation**: [Notion Guide](https://automatisation.notion.site/WhatsApp-II-20f3d6550fd980b9b2e6fe5e1a84051b?source=copy_link)\n\n---\n\n### Need help customizing?\nContact me for consulting and support : [Linkedin](https://www.linkedin.com/in/dr-firas/) / [Youtube](https:/https://www.youtube.com/@DRFIRASS)", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.webhook", - "n8n-nodes-base.switch", - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "n8n-nodes-base.googleSheetsTool", - "@n8n/n8n-nodes-langchain.toolHttpRequest", - "n8n-nodes-base.respondToWebhook", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.code", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.sms77" - ], - "tags": ["trigger:webhook", "ai", "integration:langchain"], - "triggerType": "webhook", - "hasAI": true, - "score": 84.68, - "scoreBreakdown": { - "traction": 0.586, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.591 - }, - "source": "https://n8n.io/workflows/4949", - "author": "drfiras", - "success": true - }, - { - "id": 5680, - "slug": "rag-chatbot-with-supabase-togetherai-openrouter-5680", - "name": "RAG Chatbot with Supabase + TogetherAI + Openrouter", - "description": "## ⚠️ RUN the FIRST WORKFLOW ONLY ONCE \n(as it will convert your content in Embedding format and save it in DB and is ready for the RAG Chat)\n\n## 📌 Telegram Trigger\n\n* **Type:** `telegramTrigger`\n* **Purpose:** Waits for new Telegram messages to trigger the workflow.\n* **Note:** Currently disabled.\n\n---\n\n## 📄 Content for the Training\n\n* **Type:** `googleDocs`\n* **Purpose:** Fetches document content from Google Docs using its URL.\n* **Details:** Uses Service Account authentication.\n\n---\n\n## ✂️ Splitting into Chunks\n\n* **Type:** `code`\n* **Purpose:** Splits the fetched document text into smaller chunks (1000 chars each) for processing.\n* **Logic:** Loops over text and slices it.\n\n---\n\n## 🧠 Embedding Uploaded Document\n\n* **Type:** `httpRequest`\n* **Purpose:** Calls Together AI embedding API to get vector embeddings for each text chunk.\n* **Details:** Sends JSON with model name and chunk as input.\n\n---\n\n## 🛢 Save the embedding in DB\n\n* **Type:** `supabase`\n* **Purpose:** Saves each text chunk and its embedding vector into the Supabase `embed` table.\n\n\n## SECOND WORKFLOW EXPLAINATION:\n\n## 💬 When chat message received\n\n* **Type:** `chatTrigger`\n* **Purpose:** Starts the workflow when a user sends a chat message.\n* **Details:** Sends an initial greeting message to the user.\n\n---\n\n## 🧩 Embend User Message\n\n* **Type:** `httpRequest`\n* **Purpose:** Generates embedding for the user’s input message.\n* **Details:** Calls Together AI embeddings API.\n\n---\n\n## 🔍 Search Embeddings\n\n* **Type:** `httpRequest`\n* **Purpose:** Searches Supabase DB for the top 5 most similar text chunks based on the generated embedding.\n* **Details:** Calls Supabase RPC function `matchembeddings1`.\n\n---\n\n## 📦 Aggregate\n\n* **Type:** `aggregate`\n* **Purpose:** Combines all retrieved text chunks into a single aggregated context for the LLM.\n\n---\n\n## 🧠 Basic LLM Chain\n\n* **Type:** `chainLlm`\n* **Purpose:** Passes the user's question + aggregated context to the LLM to generate a detailed answer.\n* **Details:** Contains prompt instructing the LLM to answer only based on context.\n\n---\n\n## 🤖 OpenRouter Chat Model\n\n* **Type:** `lmChatOpenRouter`\n* **Purpose:** Provides the actual AI language model that processes the prompt.\n* **Details:** Uses `qwen/qwen3-8b:free` model via OpenRouter and you can use any of your choice.", - "nodes": [ - "n8n-nodes-base.googleDocs", - "n8n-nodes-base.code", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.supabase", - "n8n-nodes-base.telegramTrigger", - "@n8n/n8n-nodes-langchain.chatTrigger", - "n8n-nodes-base.aggregate", - "@n8n/n8n-nodes-langchain.chainLlm", - "@n8n/n8n-nodes-langchain.lmChatOpenRouter", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:telegram", "ai", "integration:httpRequest"], - "triggerType": "telegram", - "hasAI": true, - "score": 84.65, - "scoreBreakdown": { - "traction": 0.528, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.818 - }, - "source": "https://n8n.io/workflows/5680", - "author": "iamvaar", - "success": true - }, - { - "id": 4881, - "slug": "turn-google-sheets-ideas-into-ai-videos-with-gpt-4o-and-fal--4881", - "name": "Turn Google Sheets Ideas into AI Videos with GPT-4o and Fal.AI Veo 3", - "description": "\n**Turn Your Ideas into Videos—Right from Google Sheets!**\n\nThis workflow helps you make cool 8-second videos using Fal.AI and Veo 3, just by typing your idea into a Google Sheet. You can even choose if you want your video to have sound or not. It’s super easy—no tech skills needed!\n\n**Why use this?**\n\nJust type your idea in a sheet—*no fancy tools or uploads.*\n\nGet a video link back in the same sheet.\n\nWorks with or without sound—*your choice!*\n\n**How does it work?**\n\nYou write your idea, pick the video shape, and say if you want sound (true or false) in the Google Sheet.\n\nn8n reads your idea and asks Fal.AI to make your video.\n\nWhen your video is ready, the link shows up in your sheet.\n\n**What do you need?**\n\n- A Google account and Google Sheets connected with service account ([check this link for reference](https://docs.n8n.io/integrations/builtin/credentials/google/))\n- A copy of the following Google Spreadsheet:\n[Spreadsheet to copy](https://docs.google.com/spreadsheets/d/100ur8DnU_q2YIAx15ccMfEWC43Di_G7hj19csjxg73A/edit?usp=sharing)\n\n- An OpenAI API key\n\n- A Fal.AI account with some money in it\n\n**That’s it!** Just add your ideas and let the workflow make the videos for you. Have fun creating!\n\nIf you have any questions, just contact me in X **[@maxrojasdelgado](https://x.com/maxrojasdelgado)**.", - "nodes": [ - "n8n-nodes-base.wait", - "n8n-nodes-base.if", - "n8n-nodes-base.httpRequest", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.set", - "n8n-nodes-base.googleSheetsTrigger", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.googleSheets" - ], - "tags": ["trigger:other", "ai", "integration:httpRequest"], - "triggerType": "other", - "hasAI": true, - "score": 84.62, - "scoreBreakdown": { - "traction": 0.537, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.778 - }, - "source": "https://n8n.io/workflows/4881", - "author": "maxr", - "success": true - }, - { - "id": 3414, - "slug": "slack-ai-chatbot-for-business-team-with-rag-claude-3-7-sonne-3414", - "name": "Slack AI Chatbot for business team with RAG, Claude 3.7 Sonnet and Google Drive", - "description": "Imagine having an **AI chatbot on Slack** that seamlessly integrates with your company’s workflow, **automating repetitive requests**. No more digging through emails or documents to find answers about IT requests, company policies, or vacation days—just ask the bot, and it will instantly provide the right information. \n\nWith its **24/7 availability**, the chatbot ensures that team members get immediate support without waiting for a colleague to be online, making assistance faster and more efficient. \n\nMoreover, this AI-powered bot serves as a **central hub for internal communication**, allowing everyone to quickly access procedures, documents, and company knowledge without searching manually. A simple Slack message is all it takes to get the information you need, enhancing productivity and collaboration across teams.\n\n-----\n\n#### **How It Works** \n1. **Slack Trigger**: The workflow starts when a user mentions the AI bot in a Slack channel. The trigger captures the message and forwards it to the AI Agent. \n2. **AI Agent Processing**: \n - The AI Agent, powered by Anthropic's Claude 3.7 Sonnet model, processes the query. \n - It uses **Retrieval-Augmented Generation (RAG)** to fetch relevant information from the company’s internal knowledge base stored in Qdrant (a vector database). \n - A **Simple Memory** buffer retains recent conversation context (last 10 messages) for continuity. \n3. **Knowledge Retrieval**: \n - The RAG tool searches Qdrant’s vector store using OpenAI embeddings to find the most relevant document chunks (top 10 matches). \n4. **Response Generation**: \n - The AI synthesizes the retrieved data into a concise, structured response (1-2 sentences for the answer, 2-3 supporting details, and a source citation). \n - The response is formatted in Slack-friendly markdown (bullet points, blockquotes) and sent back to the user. \n\n--- \n\n#### **Set Up Steps** \n1. **Prepare Qdrant Vector Database**: \n - Create a Qdrant collection via HTTP request (`Create collection` node). \n - Optionally, refresh/clear the collection (`Refresh collection` node) before adding new documents. \n2. **Load Company Documents**: \n - Fetch files from a Google Drive folder (`Get folder` → `Download Files`). \n - Process documents: Split text into chunks (`Token Splitter`) and generate embeddings (`Embeddings OpenAI2`). \n - Store embeddings in Qdrant (`Qdrant Vector Store1`). \n3. **Configure Slack Bot**: \n - Create a Slack bot via Slack API with required permissions\n - Add the bot to the desired Slack channel and note the `channelId` for the workflow. \n4. **Deploy AI Components**: \n - Connect the AI Agent to Anthropic’s model, RAG tool, and memory buffer. \n - Ensure OpenAI embeddings are configured for both RAG and document processing. \n5. **Test & Activate**: \n - Use the manual trigger (`When clicking ‘Test workflow’`) to validate document ingestion. \n - Activate the workflow to enable real-time Slack interactions. \n\n----\n\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/).", - "nodes": [ - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "@n8n/n8n-nodes-langchain.embeddingsOpenAi", - "@n8n/n8n-nodes-langchain.vectorStoreQdrant", - "@n8n/n8n-nodes-langchain.toolCalculator", - "n8n-nodes-base.slackTrigger", - "n8n-nodes-base.slack", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.googleDrive", - "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", - "@n8n/n8n-nodes-langchain.textSplitterTokenSplitter", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.lmChatAnthropic" - ], - "tags": ["trigger:other", "ai", "integration:langchain"], - "triggerType": "other", - "hasAI": true, - "score": 84.56, - "scoreBreakdown": { - "traction": 0.537, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.765 - }, - "source": "https://n8n.io/workflows/3414", - "author": "n3witalia", - "success": true - }, - { - "id": 4977, - "slug": "organize-email-attachments-from-gmail-to-structured-google-d-4977", - "name": "Organize Email Attachments from Gmail to Structured Google Drive Folders", - "description": "# Automated Binary Data Extraction from Gmail to Google Drive Folder\n\n\nThis workflow is designed to automate the process of handling emails with binary attachments. It triggers when a new email arrives in a specified Gmail account (or can be configured with a similar email trigger) and is set to download any binary attachments. The workflow then filters the email to confirm it contains binary data (attachments). If attachments are present, it proceeds to retrieve the full email details, including all binary data.\n\nA crucial step is the creation of a new Google Drive folder. This folder is dynamically named using the email's subject and the current timestamp, for example, \"[Email Subject] - [Current Timestamp]\". Following this, the workflow separates each individual attachment from the email. Finally, these attachments are uploaded into the newly created Google Drive folder, with their original filenames preserved. The overall purpose of this workflow is to automatically organize and store email attachments into a structured Google Drive folder system. This workflow is compatible with any type of binary data found in an email, as the filter is designed to detect any binary data, not just PDFs.\n\n\n**How It Works**\n\n1. **Trigger:** The workflow initiates when a new email arrives in a specified Gmail account. Alternatively, it can be configured with a similar email trigger.\n\n2. **Download Attachments:** The workflow is set to automatically download any binary attachments from the incoming email.\n\n3. **Filter Attachments:** The workflow then filters the email to confirm it contains binary data (attachments).\n\n4. **Retrieve Full Email Details:** If attachments are present, the workflow proceeds to retrieve the complete details of the email, including all binary data.\n\n5. **Create Google Drive Folder:** A new folder is created in Google Drive. This folder is dynamically named using the email's subject and the current timestamp (e.g., \"[Email Subject] - [Current Timestamp]\").\n\n6. **Split Out Attachments:** Each individual binary attachment from the email is separated into its own item within the workflow.\n\n7. **Upload to Google Drive:** Finally, these separated attachments are uploaded into the newly created Google Drive folder, retaining their original filenames.\n\n\n**Need Help? Have Questions?**\nFor consulting and support, or if you have questions, please feel free to connect with me on [LinkedIn ](https://www.linkedin.com/in/michael-gullo-46b3b7274/) or email [michael.gullo@outlook.com](michael.gullo@outlook.com). \n", - "nodes": [ - "n8n-nodes-base.gmailTrigger", - "n8n-nodes-base.googleDrive", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.filter", - "n8n-nodes-base.gmail", - "n8n-nodes-base.merge", - "n8n-nodes-base.splitOut" - ], - "tags": ["trigger:gmail", "integration:googleDrive"], - "triggerType": "gmail", - "hasAI": false, - "score": 84.53, - "scoreBreakdown": { - "traction": 0.512, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.857 - }, - "source": "https://n8n.io/workflows/4977", - "author": "mgullo", - "success": true - }, - { - "id": 4404, - "slug": "one-way-sync-between-telegram-notion-google-drive-and-google-4404", - "name": "One-way sync between Telegram, Notion, Google Drive, and Google Sheets", - "description": "# One-way sync between Telegram, Notion, Google Drive, and Google Sheets\n\n## Who is this for?\n\nThis workflow is perfect for productivity-focused teams, remote workers, virtual assistants, and digital knowledge managers who receive documents, images, or notes through Telegram and want to automatically organize and store them in Notion, Google Drive, and Google Sheets—without any manual work.\n\n## What problem is this workflow solving?\n\nManaging Telegram messages and media manually across different tools like Notion, Drive, and Sheets can be tedious. This workflow automates the classification and storage of incoming Telegram content, whether it’s a text note, an image, or a document. It saves time, reduces human error, and ensures that media is stored in the right place with metadata tracking.\n\n## What this workflow does\n\n- **Triggers on a new Telegram message** using the Telegram Trigger node.\n- **Classifies the message type** using a Switch node:\n - Text messages are appended to a Notion block.\n - Images are converted to base64, uploaded to imgbb, and then added to Notion as toggle-image blocks.\n - Documents are downloaded, uploaded to Google Drive, and the metadata is logged in Google Sheets.\n- **Sends a completion confirmation** back to the original Telegram chat.\n\n## Setup\n\n1. **Telegram Bot**: Set up a bot and get the API token.\n2. **Notion Integration**:\n - Share access to your target Notion page/block.\n - Use the Notion API credentials and block ID where content should be appended.\n3. **Google Drive & Sheets**:\n - Connect the relevant accounts.\n - Select the destination folder and spreadsheet.\n4. **imgbb API**: Obtain a free API key from [imgbb](https://api.imgbb.com/).\n\nReplace placeholder credential IDs and asset URLs as needed in the imported workflow.\n\n## How to customize this workflow to your needs\n\n- **Change Storage Locations**:\n - Update the Notion block ID or Google Drive folder ID.\n - Switch Google Sheet to log in a different file or sheet.\n- **Add More Filters**:\n - Use additional Switch rules to handle other Telegram message types (like videos or voice messages).\n- **Modify Response Message**:\n - Personalize the Telegram confirmation text based on the file type or sender.\n- **Use a different image hosting service** if you don’t want to use imgbb.\n", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.telegramTrigger", - "n8n-nodes-base.switch", - "n8n-nodes-base.telegram", - "n8n-nodes-base.extractFromFile", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.notion", - "n8n-nodes-base.googleDrive", - "n8n-nodes-base.googleSheets" - ], - "tags": ["trigger:telegram", "integration:telegram"], - "triggerType": "telegram", - "hasAI": false, - "score": 84.5, - "scoreBreakdown": { - "traction": 0.543, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.727 - }, - "source": "https://n8n.io/workflows/4404", - "author": "lakshit1996", - "success": true - }, - { - "id": 3277, - "slug": "smart-email-auto-responder-template-using-ai-3277", - "name": "Smart Email Auto-Responder Template using AI", - "description": "# Smart Email Auto-Responder with AI Classification \n## Automatically Categorize and Reply to Emails using LangChain + Google Gemini + Gmail + SMTP + Brevo\n \nThis n8n workflow is designed to intelligently manage incoming emails and automatically send personalized responses based on the content. It classifies emails using LangChain's Text Classifier, sends HTML responses depending on the category, and updates Gmail and Brevo CRM accordingly.\n\n---\n\n## Key Features \n\n**Triggers and Classifies Emails** \n- Listens for new Gmail messages every hour \n- Uses AI-based classification to identify the type of inquiry For Example: \n - Guest Post \n - YouTube Review \n - Udemy Course Inquiry\n\n**Responds Automatically** \n- Sends professional HTML replies customized for each type \n- Uses SMTP to deliver emails from your domain \n\n**Enhances Workflow with Automation** \n- Marks processed emails as read \n- Applies Gmail labels \n- Adds sender to Brevo contact list \n\n**Optional AI Chat Integration** \n- Uses Google Gemini (PaLM 2) to enhance classification or summarization \n\n---\n\n## Tools & Integrations Required \n\n1. Gmail account (OAuth2)\n2. LangChain (Text Classifier node)\n3. Google Gemini API account\n4. SMTP credentials (e.g., Gmail SMTP, Brevo, etc.)\n5. Brevo/Sendinblue account and API key\n\n---\n\n## Step-by-Step Node Guide \n\n### 1. Gmail Trigger \nPolls Gmail every hour for new emails. \nFilters out internal addresses (e.g., `@syncbricks.com`). \nAvoids replying to already-responded emails (`Re:` subject filter).\n\n### 2. LangChain Text Classifier \nUses AI to categorize the content of the email based on pre-defined categories: \n- **Guest Post** \n- **Youtube** \n- **Udemy Courses**\n\n### 3. Google Gemini (PaLM) Chat Model *(Optional)* \nProvides additional AI support to enhance classification accuracy. \nCan be used to summarize or enrich the context if needed.\n\n### 4. Email Send Nodes \nEach response category has a separate SMTP node with a custom HTML email:\n- **Guest Post Inquiry** \n- **YouTube Video Inquiry** \n- **Udemy Course Inquiry**\n\n### 5. Gmail: Mark as Read \nMarks the email so it isn’t processed again.\n\n### 6. Gmail: Apply Label \nAdds a label (e.g., `Handled by Bot`) for organization.\n\n### 7. Brevo: Create/Update Contact \nSaves the sender to your CRM for future communication or marketing.\n\n---\n\n## Email Templates Included\n\n### Guest Post Template \nIncludes pricing, website list, submission guidelines, and payment instructions.\n\n### YouTube Review Template \nIncludes package pricing, review samples, video thumbnails, and inquiry instructions. \n \n### [Step by Step Tutorial](https://youtu.be/OAu6tOwubgE)\n\n### [GET n8n Now](https://www.udemy.com/course/mastering-n8n-ai-agents-api-automation-webhooks-no-code/?referralCode=0309FD70BE2D72630C09) \n\n### [N8N COURSE](https://www.udemy.com/course/mastering-n8n-ai-agents-api-automation-webhooks-no-code/?referralCode=0309FD70BE2D72630C09) \n### [n8n Book](https://lms.syncbricks.com/books/n8n/) \n \n \n\nMore courses: \n[http://lms.syncbricks.com](http://lms.syncbricks.com)\n\nYouTube Channel: \n[https://youtube.com/@syncbricks](https://youtube.com/@syncbricks)\n\n---\n\n## How to Use\n\n1. Import the template into your n8n instance.\n2. Configure your Gmail OAuth2 and SMTP credentials.\n3. Set up your LangChain Text Classifier and Google Gemini API credentials.\n4. Update label ID in the Gmail node and ensure all custom fields like `from.value[0].name` match your use case.\n5. Run the workflow and watch it respond intelligently to new inquiries.\n\n---\n\n## Best Practices\n\n- Always test with mock emails first.\n- Keep the Google Gemini node optional if you want to reduce cost/API calls.\n- Use Gmail filters to auto-label certain types of emails.\n- Monitor your Brevo contacts to track new leads.\n\n---\n\n## Attribution & Support\n\n**Developed by Amjid Ali** \nThis template took extensive time and effort to build. If you find it useful, please consider supporting my work.\n\n**Buy My Book:** \n[Mastering n8n on Amazon](https://www.amazon.com/dp/B0F23GYCFW)\n\n**Full Courses & Tutorials:** \n[http://lms.syncbricks.com](http://lms.syncbricks.com)\n\n**Follow Me Online:** \nLinkedIn: [https://linkedin.com/in/amjidali](https://linkedin.com/in/amjidali) \nWebsite: [https://amjidali.com](https://amjidali.com) \nYouTube: [https://youtube.com/@syncbricks](https://youtube.com/@syncbricks) \n", - "nodes": [ - "n8n-nodes-base.gmailTrigger", - "@n8n/n8n-nodes-langchain.textClassifier", - "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", - "n8n-nodes-base.emailSend", - "n8n-nodes-base.gmail", - "n8n-nodes-base.sendInBlue", - "n8n-nodes-base.if", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:gmail", "ai", "integration:emailSend"], - "triggerType": "gmail", - "hasAI": true, - "score": 84.44, - "scoreBreakdown": { - "traction": 0.563, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.636 - }, - "source": "https://n8n.io/workflows/3277", - "author": "amjid", - "success": true - }, - { - "id": 14020, - "slug": "build-an-ai-confidence-coach-for-women-with-gpt-4o-google-sh-14020", - "name": "Build an AI confidence coach for women with GPT-4o, Google Sheets and Gmail", - "description": "## 📊 Description\nMost career advice is generic. This workflow builds a fully personalized AI coaching system that remembers every user, adapts to their career stage and goals, detects what kind of help they need, and gets more contextual with every conversation. It is not a simple chatbot — it is a structured coaching engine with user profiling, conversation memory, intent routing, and proactive weekly outreach.\nBuilt for women's communities, coaching platforms, HR teams, and edtech creators who want to deliver real personalized career support at scale without hiring a team of coaches.\n\n## What This Workflow Does\n💬 Opens a chat interface where users can start talking immediately — no signup required\n🧮 Detects if the user is new or returning on every message using Google Sheets as a user database\n📋 Walks new users through a 4-step onboarding — name, career stage, biggest challenge, and monthly goal\n🗂️ Stores and updates every user profile in Google Sheets with full onboarding state tracking\n🔍 Detects intent from every coaching message across 6 categories — salary negotiation, interview prep, career change, leadership, confidence building, and work-life balance\n🤖 Routes each message to a topic-specific GPT-4o system prompt tailored to that coaching category\n💬 Loads the last 5 conversations from history to give GPT-4o full context before generating a response\n✅ Responds with personalized advice, one actionable step for today, and a follow-up question to keep momentum\n📝 Logs every conversation to Google Sheets with timestamp, message, intent, and AI response\n📧 Sends every fully onboarded user a personalized weekly check-in email every Sunday with a weekly challenge, progress acknowledgment, and motivational quote from a woman leader\n\n## Key Benefits\n✅ Full conversation memory — every session builds on the last\n✅ Intent detection across 6 coaching categories — not one generic prompt\n✅ User profiling — advice is always tailored to their stage, challenge, and goal\n✅ Proactive weekly outreach — not just reactive coaching\n✅ Complete audit trail — every conversation logged to Google Sheets\n✅ Works for any number of users simultaneously via session-based identification\n✅ No login or signup needed — just open the chat URL and start\n\n## How It Works\n\nSW1 — Onboarding Every message hits the Chat Trigger and gets routed through the onboarding engine. The workflow reads all users from Google Sheets and matches the current session ID. If the user is new they go through 4 onboarding steps one message at a time — name, career stage with a numbered menu, biggest challenge with a numbered menu, and their monthly goal in their own words. Numbered responses are automatically mapped to their full label so 2 becomes Early Career (1-3 years) and 5 becomes Work-life balance. Every step is saved and the onboarding state is tracked so users can return to the same conversation and pick up exactly where they left off.\n\nSW2 — Coaching Engine Once onboarding is complete every subsequent message goes straight to the coaching engine. The workflow reads the user's last 5 conversations from the Conversation Log sheet for context. It then scans the message for intent keywords across 6 categories and selects the matching system prompt. GPT-4o receives the full user profile, conversation history, and topic-specific coaching instructions before generating a response. Every response includes a main coaching answer, one specific action the user can take today, and a follow-up question to continue the conversation. The full exchange is logged to the Conversation Log sheet.\n\nSW3 — Weekly Check-in Every Sunday at 10AM the workflow reads all fully onboarded users, pulls their recent conversation topics from the log, and generates a personalized weekly check-in email for each one via GPT-4o. The email includes a warm personalized greeting, acknowledgment of their progress, a concrete weekly challenge tied to their goal, and a motivational quote from a real woman leader relevant to their situation. Emails are sent via Gmail and every send is logged to the Weekly Checkins sheet.\n\n## Features\n- n8n Chat Trigger with public URL — shareable with any user\n- Session-based user identification — no login required\n- 4-step guided onboarding with numbered menu options\n- Numbered response to label mapping for clean data storage\n- New vs returning user detection on every message\n- Google Sheets as full user database and conversation memory\n- 6-category intent detection engine with keyword matching\n- Topic-specific GPT-4o system prompts per coaching category\n- Last 5 message context window passed to every GPT call\n- Structured JSON responses — advice, action step, follow-up question\n- Weekly Sunday proactive check-in via Gmail\n- Personalized HTML email with challenge box and quote section\n- Full logging across 3 sheets — User Profiles, Conversation Log, Weekly Checkins\n\n## Requirements\n- OpenAI API key (GPT-4o access)\n- Google Sheets OAuth2 connection\n- Gmail OAuth2 connection\n- A configured Google Sheet with 3 sheets as above\n\n## Setup Steps\n- Create a Google Sheet called \"AI Confidence Coach\" with 3 sheets — User Profiles, Conversation Log, Weekly Checkins\n- Paste your Sheet ID into all Google Sheets nodes\n- Add your Google Sheets OAuth2 credentials\n- Add your OpenAI API key\n- Add your Gmail OAuth2 credentials\n- Add your email as fallback in the Send Weekly Checkin Email node\n- Activate the workflow and copy the Chat \n- Trigger URL\n- Open the chat URL and test the full onboarding flow with 5 messages\n- Send a coaching question and confirm GPT-4o responds with personalized advice\n- Add your email to the User Profiles sheet and run SW3 manually to test the weekly check-in email\n- Share the chat URL with your users — the workflow runs itself from here\n\n## Target Audience\n🤖 Women's communities and career platforms who want to offer AI coaching at scale\n💼 HR teams building internal confidence and career development tools\n🎓 Edtech creators running women's upskilling and mentorship programs\n🔧 Automation agencies building AI coaching products for clients\n", - "nodes": [ - "n8n-nodes-base.code", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.gmail", - "@n8n/n8n-nodes-langchain.chatTrigger", - "n8n-nodes-base.if", - "@n8n/n8n-nodes-langchain.chat", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.filter", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:chatTrigger", "ai", "integration:googleSheets"], - "triggerType": "chatTrigger", - "hasAI": true, - "score": 84.41, - "scoreBreakdown": { - "traction": 0.623, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.391 - }, - "source": "https://n8n.io/workflows/14020", - "author": "rahul08", - "success": true - }, - { - "id": 5368, - "slug": "manage-google-calendar-via-whatsapp-with-gpt-4-virtual-assis-5368", - "name": "Manage Google Calendar via WhatsApp with GPT-4 Virtual Assistant", - "description": "How it works\n• Allows users to manage their Google Calendar via WhatsApp using natural language\n• Handles event creation, updates, deletions, availability checks, and agenda overviews\n• AI agent interprets the user’s message and triggers the appropriate calendar action\n• Responses are sent back to the user via WhatsApp, with confirmation or schedule info\n\nSet up steps\n• Set up a WhatsApp Business Cloud account and configure your webhook\n• Connect your Google Calendar using n8n credentials\n• Deploy OpenAI API key for natural language understanding\n• Link each calendar action (create, update, delete, search) to the TimePilot agent\n• Customize confirmation messages and automate reply formatting\n\nNote: More detailed configuration and custom logic are described inside sticky notes within the workflow.", - "nodes": [ - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.set", - "n8n-nodes-base.googleCalendarTool", - "n8n-nodes-base.whatsAppTrigger", - "n8n-nodes-base.whatsApp", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "@n8n/n8n-nodes-langchain.agent", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:other", "ai", "integration:googleCalendarTool"], - "triggerType": "other", - "hasAI": true, - "score": 84.41, - "scoreBreakdown": { - "traction": 0.545, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.7 - }, - "source": "https://n8n.io/workflows/5368", - "author": "floyd", - "success": true - }, - { - "id": 11532, - "slug": "automated-ai-voice-cloning-from-youtube-videos-to-elevenlabs-11532", - "name": "Automated AI Voice Cloning 🤖🎤 from YouTube videos to ElevenLabs & Google Sheets", - "description": "This workflow automates the process of **creating cloned voices** in **ElevenLabs** using audio extracted from **YouTube** videos. It processes a list of video URLs from Google Sheets, converts them to audio, submits them to [ElevenLabs for voice cloning](https://try.elevenlabs.io/ahkbf00hocnu)*, and records the generated voice IDs back to the spreadsheet.\n\n*ONLY FOR STARTER, CREATOR, PRO PLAN\n\n\n\n**Important Considerations for Best Results:**\n\nFor optimal voice cloning quality with ElevenLabs, carefully select your source YouTube videos:\n- **Duration**: Choose videos that are sufficiently long (preferably 1-5 minutes of clear speech) to provide enough audio data for accurate voice modeling.\n- **Audio Quality**: Select videos with high-quality audio, minimal background noise, and clear vocal recording.\n- **Single Speaker**: Use videos featuring only **one primary speaker**. Multiple voices in the same audio will confuse the cloning algorithm and produce poor results.\n- **Consistent Voice**: Ensure the speaker maintains a consistent tone and speaking style throughout the clip for the most faithful reproduction.\n\n---\n\n\n### **Key Features**\n\n#### **1. ✅ Fully Automated Voice Creation Workflow**\n\n* No manual downloading, converting, or uploading is required.\n* Just paste the YouTube link and voice name into the sheet—everything else happens automatically.\n\n#### **2. ✅ Seamless Audio Extraction**\n\nUsing RapidAPI ensures:\n\n* High success rate in extracting audio\n* Support for virtually any YouTube video\n* Consistent output format required by ElevenLabs\n\n#### **3. ✅ Hands-Off ElevenLabs Voice Creation**\n\nThe workflow handles all the steps required by the ElevenLabs API, including:\n\n* Uploading binary audio\n* Naming voices\n* Capturing and storing the resulting voice ID\n\nThis is much faster than the manual method inside the ElevenLabs dashboard.\n\n#### **4. ✅ Centralized, Reusable Setup**\n\nOnce the API keys are added:\n\n* The same workflow can be reused indefinitely\n* Users don’t need technical skills\n* Updating only requires editing the sheet\n\n---\n\n### **How it works:**\n\n1. **Data Retrieval**: The workflow starts by fetching data from a Google Sheets spreadsheet that contains YouTube video URLs in the \"YOUTUBE VIDEO\" column and desired voice names in the \"VOICE NAME\" column. It specifically targets rows where the \"ELEVENLABS VOICE ID\" field is empty, ensuring only unprocessed videos are handled.\n\n2. **Video Processing Pipeline**:\n - **Video ID Extraction**: Each YouTube URL is parsed to extract the unique video identifier using a regular expression.\n - **Audio Conversion**: The video ID is sent to the RapidAPI \"YouTube MP3 2025\" service, which converts the YouTube video to an audio file (M4A format).\n - **Audio Download**: The resulting audio file is downloaded locally for processing.\n\n3. **Voice Creation**: The downloaded audio file is submitted to ElevenLabs API via a POST request to the `/v1/voices/add` endpoint. This creates a new voice clone based on the audio sample. The voice name is currently hardcoded as \"Teresa Mannino\" in the workflow but should be dynamically configured to use the value from the \"VOICE NAME\" spreadsheet column.\n\n4. **Data Update**: The workflow captures the `voice_id` returned by ElevenLabs and writes it back to the corresponding row in the Google Sheets spreadsheet in the \"ELEVENLABS VOICE ID\" column, completing the processing cycle for that video.\n\n---\n\n### **Set up steps:**\n\n1. **Prepare the Data Sheet**: Duplicate the provided Google Sheets template. Fill in the \"YOUTUBE VIDEO\" column with YouTube URLs and the \"VOICE NAME\" column with your desired names for the cloned voices. Ensure your videos meet the quality criteria mentioned above.\n\n2. **Configure APIs**:\n - **RapidAPI**: Sign up for a free trial API key from the \"YouTube MP3 2025\" service on RapidAPI. Enter this key into the `x-rapidapi-key` header field in the \"From video to audio\" node.\n - **ElevenLabs**: Generate an API key from your ElevenLabs account. Configure the \"Create voice\" node's HTTP Header Authentication with the name `xi-api-key` and your ElevenLabs API key as the value.\n\n3. **Optional Customization**: Modify the \"Create voice\" node to use the dynamic voice name from your spreadsheet instead of the hardcoded \"Teresa Mannino\" value for more flexible operation.\n\n4. **Execute**: Run the workflow. It will automatically process each qualifying row, create voices in ElevenLabs, and populate the spreadsheet with the new Voice IDs. Monitor the workflow execution to ensure successful processing of each video.\n\n---\n\n👉 [Subscribe to my new **YouTube channel**](https://youtube.com/@n3witalia). Here I’ll share videos and Shorts with practical tutorials and **FREE templates for n8n**.\n\n[![image](https://n3wstorage.b-cdn.net/n3witalia/youtube-n8n-cover.jpg)](https://youtube.com/@n3witalia)\n\n---\n\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/). ", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.code", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:manual", "integration:httpRequest"], - "triggerType": "manual", - "hasAI": false, - "score": 84.35, - "scoreBreakdown": { - "traction": 0.561, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.625 - }, - "source": "https://n8n.io/workflows/11532", - "author": "n3witalia", - "success": true - }, - { - "id": 13353, - "slug": "ingest-and-enrich-q-a-pairs-then-store-in-data-table-1-2-13353", - "name": "Ingest and enrich Q&A pairs then store in Data Table [1/2]", - "description": "This workflow serves an n8n Form to end user to capture a Question and Answer pair (to be used by an Q&A AI Agent in a separate workflow).\n\nThe workflow:\n- Appends a `isTrusted` field depending on email domain\n- Adds AI-generated tags relevant to the Question and Answer pair\n- Stores the Q&A pair in an n8n Data Table \n\nThis template is part of the official n8n quick start tutorial (2026). [Watch it here](https://www.youtube.com/watch?v=GuaKeDS6UKU).\n", - "nodes": [ - "n8n-nodes-base.formTrigger", - "n8n-nodes-base.if", - "n8n-nodes-base.set", - "n8n-nodes-base.noOp", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.chainLlm", - "n8n-nodes-base.dataTable", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:formTrigger", "ai", "integration:set"], - "triggerType": "formTrigger", - "hasAI": true, - "score": 84.32, - "scoreBreakdown": { - "traction": 0.497, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.875 - }, - "source": "https://n8n.io/workflows/13353", - "author": "max-n8n", - "success": true - }, - { - "id": 4334, - "slug": "tradingview-signal-extractor-with-gmail-google-sheets-telegr-4334", - "name": "TradingView Signal Extractor with Gmail, Google Sheets & Telegram Notifications", - "description": "Stay ahead in your **trading game** with this powerful **n8n automation** workflow. Designed for real-time efficiency, this setup continuously **scans your Gmail inbox** for trading alerts from **TradingView** and ensures **you never miss a signal.**\n\nEvery minute, this workflow will:\n\n📩 **Check Gmail** for new messages using a trigger.\n🔍 **Identify** emails coming specifically from TradingView.\n📊 **Extract** key trading signals like **BUY** or **SELL** along with the company name.\n🕒 **Capture** the exact date and time of the alert.\n📄 **Log** all extracted signals neatly into a **Google Sheet** for easy tracking and analysis.\n📲 **Instantly notify you on Telegram** with a custom message so you can act fast.\n\nPerfect for **traders** who want an **automated assistant** to handle **alerts, record them for analysis,** and provide timely updates — **all without lifting a finger.**", - "nodes": [ - "n8n-nodes-base.code", - "n8n-nodes-base.gmail", - "n8n-nodes-base.telegram", - "n8n-nodes-base.if", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.dateTime", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.gmailTrigger" - ], - "tags": ["trigger:gmail", "integration:code"], - "triggerType": "gmail", - "hasAI": false, - "score": 84.22, - "scoreBreakdown": { - "traction": 0.536, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.7 - }, - "source": "https://n8n.io/workflows/4334", - "author": "itechdp", - "success": true - }, - { - "id": 4105, - "slug": "generate-invoices-save-to-drive-and-send-email-to-customer-w-4105", - "name": "Generate Invoices, Save to Drive and Send Email to Customer with JS + G Sheets", - "description": "This workflow automates invoice generation from form submissions, ensuring unique order IDs, creating PDF invoices, storing files, emailing customers, and logging invoice data — all seamlessly integrated.\n\n---\n\n## 🔹 Workflow Overview\n\n1. **Trigger (Webhook)**\n Starts when an order form is submitted, capturing customer and order details.\n\n2. **Generate Random Order ID**\n A Function node creates a unique alphanumeric invoice ID (e.g., `INV-X92B7D`).\n\n3. **Check for Duplicate Order ID**\n Google Sheets looks up the generated order ID in your invoice log sheet to prevent duplicates.\n\n4. **Conditional Check (IF Node)**\n\n * If the ID already exists → regenerates a new ID (loops back)\n * If unique → proceeds to invoice creation\n\n5. **Prepare Invoice Data**\n A Set node formats customer info, date, order items, and the unique order ID to fit your invoice template.\n\n6. **Convert HTML to PDF**\n HTTP Request node sends your invoice HTML to the RapidAPI HTML-to-PDF service and receives the PDF file.\n\n7. **Upload PDF to Cloud Storage**\n Save the PDF in Google Drive or Dropbox with a clear file name like `Invoice-INV-X92B7D.pdf`.\n\n8. **Send Invoice Email to Customer**\n Email node attaches the PDF and includes the order ID in the email subject/body.\n\n9. **Log Invoice Details**\n Append invoice data (customer info, order ID, total, PDF link) to your Google Sheet for tracking.\n\n---\n\n## ⚙️ Node Details & Setup\n\n### 1. Webhook Trigger\n\n* Configure to receive form submissions (order details like name, email, items, total).\n\n### 2. Function: Generate Random Order ID\n\n* Sample JS code generates unique IDs prefixed by `INV-`.\n\n### 3. Google Sheets: Lookup Row\n\n* Set up connection to your invoice log sheet.\n* Search for existing order ID to avoid duplicates.\n\n### 4. IF Node: Check Order ID Existence\n\n* Condition: If order ID found → loop to regenerate.\n* Else → continue workflow.\n\n### 5. Set Node: Prepare Invoice HTML\n\n* Define variables like customer name, date, items, and order ID.\n* This data populates your HTML invoice template.\n\n### 6. HTTP Request: Convert HTML to PDF\n\n* [API URL to get your key](https://rapidapi.com/rhodium/api/html2pdf2/playground/apiendpoint_9b1017c6-2f56-4b7e-80d5-0fe24937d398)\n* Send invoice HTML in the request body.\n* Receive PDF file blob or download URL.\n\n### 7. Google Drive (or Dropbox) Upload\n\n* Upload the PDF file.\n* Use file name format: `Invoice-{{$json[\"order_id\"]}}.pdf`\n\n### 8. Email Node\n\n* Recipient: customer email from the form data.\n* Attach generated PDF.\n* Include order ID in email subject or body for reference.\n\n### 9. Google Sheets: Append Row\n\n* Log invoice metadata to keep records updated.\n\n---\n\n## 📁 Google Sheets Template\n\nYou can make a copy of the invoice log template [here](https://docs.google.com/spreadsheets/d/1QW_Lg1aoEBku8GaxwPbZfBY5ITAuSdvoAXRyNdCEujM/edit?gid=0)\n\n`This sheet includes columns for order\\_id, customer name, email, total, and invoice PDF link. Customize it as needed.`\n\n---\n\n## 📌 Additional Notes\n\n* Customize the invoice HTML template inside the Set node to match your branding.\n* Ensure API credentials for RapidAPI, Google Drive/Dropbox, and email are properly set up in your n8n credentials.\n* You can expand this workflow by adding payment processing or SMS notifications.\n\n---\n\n## Need help or want a custom workflow?\n\nReach out via email at [joseph@uppfy.com](mailto:joseph@uppfy.com).", - "nodes": [ - "n8n-nodes-base.webhook", - "n8n-nodes-base.code", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.if", - "n8n-nodes-base.set", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.googleDrive", - "n8n-nodes-base.emailSend", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:webhook", "integration:code"], - "triggerType": "webhook", - "hasAI": false, - "score": 84.18, - "scoreBreakdown": { - "traction": 0.527, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.727 - }, - "source": "https://n8n.io/workflows/4105", - "author": "mjomba", - "success": true - }, - { - "id": 3446, - "slug": "daily-newsletter-service-using-excel-outlook-and-ai-3446", - "name": "Daily Newsletter Service using Excel, Outlook and AI", - "description": "### This n8n template builds a newsletter (\"daily digest\") delivery service which pulls and summarises the latest n8n.io template in select categories defined by subscribers.\n\nIt's scheduled to run once a day and sends the newsletter directly to subscriber via a nicely formatted email. If you've had trouble keeping up with the latest and greatest templates beign published daily, this workflow can save you a lot of time!\n\n### How it works\n* A scheduled trigger pulls a list of subscribers (email and category preferences) from an Excel workbook.\n* We work out unique categories amongst all subscribers and only fetch the latest n8n website templates from these categories to save on resources and optimise the number of API calls we make.\n* The fetched templates are summarised via AI to produce a short description which is more suitable for our email format.\n* For each subscriber, we filter and collect only the templates relevant to their category preferences (as defined in the Excel) and ensure that duplicate templates or those which have been \"seen before\" are omitted.\n* A HTML node is then used to generate the email newsletter. HTML emails are the perfect format since we can add links back to the template.\n* Finally, we use the Outlook node to send the email digest to the subscriber.\n\n### How to use\n* Populate your Excel sheet with 3 columns: name, email and categories. Categories is a comma-delimited list of categories which match the n8n template website. The available categories are AI, SecOps, Sales, IT Ops, Marketing, Engineering, DevOps, Building Blocks, Design, Finance, HR, Other, Product and Support.\n* To subscribe a new user, simply add their email to the Excel sheet with at least one category.\n* To unsubscribe a user, remove them from the sheet.\n* If you're not interested in paid templates, you may want to filter them out after fetching.\n\n### Requirements\n* Microsoft Excel for subscriber list\n* Microsoft Outlook for delivering emails\n* OpenAI for AI-generated descriptions\n\n### Customising the workflow\n* Use AI to summarise the week's trend of templates types and use-cases\n* This template can be the basis for other similar newsletters - just pull in a list of things from anywhere!", - "nodes": [ - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.microsoftExcel", - "n8n-nodes-base.set", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "n8n-nodes-base.aggregate", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.splitInBatches", - "@n8n/n8n-nodes-langchain.chainLlm", - "n8n-nodes-base.merge", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.noOp", - "n8n-nodes-base.removeDuplicates", - "n8n-nodes-base.if", - "n8n-nodes-base.html", - "n8n-nodes-base.microsoftOutlook", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:schedule", "ai", "integration:set"], - "triggerType": "schedule", - "hasAI": true, - "score": 84.15, - "scoreBreakdown": { - "traction": 0.557, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.6 - }, - "source": "https://n8n.io/workflows/3446", - "author": "jimleuk", - "success": true - }, - { - "id": 3796, - "slug": "auto-generate-meeting-attendee-research-with-gpt-4o-google-c-3796", - "name": "Auto-Generate Meeting Attendee Research with GPT-4o, Google Calendar, and Gmail", - "description": "### How it works:\n\nWhenever a new event is scheduled on your Google Calendar, this workflow generates a Meeting Briefing email, giving an overview of each person on the call and the company they work for.\n\nIt makes use of the [web search](https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses) tool on the OpenAI Responses API to make lookups.\n\nThe workflow triggers when a new event is added to the calendar, loops over each attendee, generating reports on each person and their company, collates the results, and sends the briefing as an email.\n\n### Set up steps:\n\n- Add your credentials for Google Calendar (for viewing events) and Gmail (to send the email)\n- Add your OpenAI credentials as a Header Auth on the Company Search and Person Search nodes.\n\t- Name: Authorization\n\t- Value: Bearer {{ YOUR_API_KEY }}\n- Edit the \"Edit Fields\" node with the email that you want to send the briefing to, and a short bit of context about yourself.", - "nodes": [ - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.googleCalendarTrigger", - "n8n-nodes-base.filter", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.if", - "n8n-nodes-base.aggregate", - "n8n-nodes-base.set", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.markdown", - "n8n-nodes-base.gmail" - ], - "tags": ["trigger:other", "integration:set"], - "triggerType": "other", - "hasAI": false, - "score": 84.14, - "scoreBreakdown": { - "traction": 0.529, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.714 - }, - "source": "https://n8n.io/workflows/3796", - "author": "adamjanes", - "success": true - }, - { - "id": 11205, - "slug": "generate-marketing-ad-banners-with-line-gemini-and-nano-bana-11205", - "name": "Generate Marketing Ad Banners with LINE, Gemini, and Nano Banana Pro", - "description": "## About This Template\nThis workflow creates high-quality, text-rich advertising banners from simple LINE messages. \n\nIt combines **Google Gemini** (for marketing-focused prompt engineering) and **Nano Banana Pro** (accessed via Kie.ai API) to generate images with superior text rendering capabilities. It also handles the asynchronous API polling required for high-quality image generation.\n\n## How It Works\n1. **Input:** Users send a banner concept via LINE (e.g., \"Coffee brand, morning vibe\").\n2. **Prompt Engineering:** Gemini optimizes the request into a detailed prompt, specifying lighting, composition, and Japanese catch-copy placement.\n3. **Async Generation:** The workflow submits a job to Nano Banana Pro (Kie API) and intelligently waits/polls until the image is ready.\n4. **Hosting:** The final image is downloaded and uploaded to a public AWS S3 bucket.\n5. **Delivery:** The image is pushed back to the user on LINE.\n\n## Who It’s For\n* Marketing teams creating A/B test assets.\n* Japanese market advertisers needing accurate text rendering.\n* Developers looking for an example of **Async API Polling** patterns in n8n.\n\n## Requirements\n* **n8n** (Cloud or Self-hosted).\n* **Kie.ai API Key** (for Nano Banana Pro model).\n* **Google Gemini API Key**.\n* **AWS S3 Bucket** (Public access enabled).\n* **LINE Official Account** (Messaging API).\n\n## Setup Steps\n1. **Credentials:** Configure the \"Header Auth\" credential for the Kie.ai nodes (Header: `Authorization`, Value: `Bearer YOUR_API_KEY`).\n2. **AWS:** Ensure your S3 bucket allows public read access so LINE can display the image.\n3. **Webhook:** Add the production webhook URL to your LINE Developers console.", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.webhook", - "n8n-nodes-base.code", - "@n8n/n8n-nodes-langchain.googleGemini", - "n8n-nodes-base.wait", - "n8n-nodes-base.awsS3" - ], - "tags": ["trigger:webhook", "ai", "integration:httpRequest"], - "triggerType": "webhook", - "hasAI": true, - "score": 84.01, - "scoreBreakdown": { - "traction": 0.575, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.5 - }, - "source": "https://n8n.io/workflows/11205", - "author": "pippi", - "success": true - }, - { - "id": 3772, - "slug": "automatically-classify-and-label-gmail-emails-with-google-ge-3772", - "name": "Automatically Classify and Label Gmail Emails with Google Gemini AI", - "description": "### Description\nQuickly organize your inbox with AI!\nThis simple workflow automatically classifies incoming emails into different categories — like High Priority, Work Related, or Promotions — and applies Gmail labels accordingly.\nSetup takes less than 2 minutes, and it runs 24/7, helping you stay focused on what matters most without manual sorting.\n\n### Tools/Services Needed\n* Gmail: To trigger the workflow and label emails.\n* Google Gemini (or any LLM Model): To intelligently classify email content.\n\n\n### How It Works\n1. Gmail Trigger: Detects every new incoming email.\n\n\n2. Text Classifier Node: Classifies the email content into predefined categories.\n\n\n3. Google Gemini Chat Model: Provides the AI-powered understanding behind the classification.\n4. Conditional Labeling:\n- If the email is High Priority, label it accordingly.\n- If it’s Work Related (e.g., internal emails), apply the work label.\n- If it’s a Promotion, sort it into the promotions label.\n5. Gmail Labeling: Automatically adds the correct label to the email.\n\n### Setup Instructions\n* Connect your Gmail account to n8n.\n* Connect your Google Gemini (or other LLM) credentials.\n* Customize the categories and labels if needed.\n* Activate the workflow — and that's it!\n\n### Notes\n* You can easily add more categories (like \"Finance,\" \"Newsletters,\" etc.) by adjusting the classification prompt.\n* Works best with a clean and minimal set of categories to avoid overlap.\n* Can be adapted to work with any other large language model (OpenAI, Claude, etc.) if preferred.", - "nodes": [ - "n8n-nodes-base.gmailTrigger", - "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.lmChatGroq", - "n8n-nodes-base.gmail", - "@n8n/n8n-nodes-langchain.textClassifier", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:gmail", "ai", "integration:langchain"], - "triggerType": "gmail", - "hasAI": true, - "score": 83.97, - "scoreBreakdown": { - "traction": 0.532, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.667 - }, - "source": "https://n8n.io/workflows/3772", - "author": "guitarpmacc", - "success": true - }, - { - "id": 4031, - "slug": "cold-outreach-automation-scrape-local-leads-with-dumpling-ai-4031", - "name": "Cold Outreach Automation: Scrape Local Leads with Dumpling AI & Call via Vapi", - "description": "\n### Who is this for?\n\nThis template is for sales teams, agencies, or local service providers who want to quickly generate cold outreach lists and automatically call local businesses with a Vapi AI assistant. It’s perfect for automating cold calls from scraped local listings with no manual dialing or research.\n\n---\n\n### What problem is this workflow solving?\n\nFinding leads and initiating outreach calls can be time-consuming. This workflow automates the process: it scrapes business listings from Google Maps using Dumpling AI, extracts phone numbers, filters out incomplete data, formats the numbers, and uses Vapi to make outbound AI-powered calls. Every call is logged in Google Sheets for follow-up and tracking.\n\n---\n\n### What this workflow does\n\n1. Starts manually and pulls search queries (e.g., \"plumbers in Austin\") from Google Sheets.\n2. Sends each query to Dumpling AI’s Google Maps scraping endpoint.\n3. Splits the returned business data into individual leads.\n4. Extracts key info like business name, website, and phone number.\n5. Filters to only keep leads with valid phone numbers.\n6. Formats phone numbers for Vapi dialing (adds +1).\n7. Calls each business using Vapi AI.\n8. Logs each successful call in a Google Sheet.\n\n---\n\n### Setup\n\n1. **Google Sheets Setup**\n - Create a sheet with business search queries in the first column (e.g., `best+restaurants+in+Chicago`)\n - Make sure the tab name is set and authorized in your credentials.\n - Connect your Google Sheets account in the `Get Search Keywords from Google Sheets` node.\n\n2. **Dumpling AI Setup**\n - Go to [dumplingai.com](https://www.dumplingai.com/) \n - Generate an API Key and connect it as a header token in the `Scrape Google Map Businesses using Dumpling AI` node\n\n3. **Vapi Setup**\n - Sign into [Vapi](https://vapi.ai) and create an assistant\n - Get your `assistantId` and `phoneNumberId`\n - Insert these into the JSON payload of the `Initiate Vapi AI Call to Business` node\n - Add your Vapi API key to the credentials section\n\n4. **Call Logging**\n - Create another tab in your sheet (e.g., “leads”) with these headers:\n - `company name`\n - `phone number`\n - `website`\n - This will be used in the `Log Called Business Info to Sheet` node\n\n---\n\n### How to customize this workflow to your needs\n\n- Modify the business search terms in your Google Sheet to target specific industries or locations.\n- Add filters to exclude certain businesses based on ratings, keywords, or location.\n- Update your Vapi assistant script to match the type of outreach or pitch you’re using.\n- Add additional integrations (e.g., CRM logging, Slack notifications, follow-up emails).\n- Change the trigger to run on a schedule or webhook instead of manually.\n\n---\n\n### Nodes and Functions Breakdown\n\n- `Start Workflow Manually`: Initiates the automation manually for testing or controlled runs.\n- `Get Search Keywords from Google Sheets`: Reads search phrases from the spreadsheet.\n- `Scrape Google Map Businesses using Dumpling AI`: Sends each search query to Dumpling AI and receives matching local business data.\n- `Split Each Business Result`: Breaks the returned array of businesses into individual records for processing.\n- `Extract Business Name, Phone and website`: Extracts title, phone, and website from each business record.\n- `Filter Valid Phone Numbers Only`: Ensures only entries with a phone number move forward.\n- `Format Phone Number for Calling`: Adds a +1 country code and strips non-numeric characters.\n- `Initiate Vapi AI Call to Business`: Uses the business name and number to initiate a Vapi AI outbound call.\n- `Log Called Business Info to Sheet`: Appends business details into a Google Sheet for tracking.\n\n---\n\n### Notes\n\n- You must have valid API keys and authorized connections for Dumpling AI, Google Sheets, and Vapi.\n- Make sure to handle API rate limits if you're running the workflow on large datasets.\n- This workflow is optimized for US-based leads (+1 country code); adjust the formatting node if calling internationally.\n\n", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.set", - "n8n-nodes-base.filter" - ], - "tags": ["trigger:manual", "integration:googleSheets"], - "triggerType": "manual", - "hasAI": false, - "score": 83.96, - "scoreBreakdown": { - "traction": 0.531, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.667 - }, - "source": "https://n8n.io/workflows/4031", - "author": "yang", - "success": true - }, - { - "id": 3638, - "slug": "build-your-own-custom-api-mcp-server-3638", - "name": "Build your own CUSTOM API MCP server", - "description": "### This n8n demonstrates how any organisation can quickly and easily build and offer MCP servers to their customers or internal staff to improve productivity.\n\nThis MCP example uses PayCaptain.com as an example and shows how to create an MCP server which can search for and update employee data.\n\n### How it works\n* A MCP server trigger is used and connected to 3 custom workflow tools: Search Employee, Get Employee by ID and Update Employee.\n* Each tool makes calls to the PayCaptain API to perform their respective tasks. Extra care is performed to strip out sensitive data and ensure we're not sharing too much.\n* The Update Employee too also guards against updating fields which would preferably remain readonly. When you control the MCP server, you can determine behaviour of the tool.\n* Finally, a Google Sheet node is used to log all operations for later audit. This will add a tiny bit of latency but recommended if sensitive data is being accessed.\n\n### How to use\n* This MCP server allows any compatible MCP client to manage their PayCaptain employee database. You will need to have a PayCaptain account and developer key to use it.\n* Connect your MCP client by following the n8n guidelines here - https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-langchain.mcptrigger/#integrating-with-claude-desktop\n* Try the following queries in your MCP client:\n * \"When did Sarah start here employment at the company?\"\n * \"Does Jack work Wednesdays or Fridays?\"\n * \"Please update Tracy's NI number to ABCD123456\"\n\n### Requirements\n* PayCaptain Account and Developer Key.\n* Google Sheets to log actions for later audit.\n* MCP Client or Agent for usage such as Claude Desktop - https://claude.ai/download\n\n### Customising this workflow\n* Add or remove employee attributes as required for your user case.\n* If Google Sheets is too slow, consider an API call to a faster service to log calls to the MCP server.\n* Remember to set the MCP server to require credentials before going to production and sharing this MCP server with others!", - "nodes": [ - "n8n-nodes-base.executeWorkflowTrigger", - "n8n-nodes-base.switch", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.mcpTrigger", - "@n8n/n8n-nodes-langchain.toolWorkflow", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.filter", - "n8n-nodes-base.aggregate", - "n8n-nodes-base.set", - "n8n-nodes-base.if" - ], - "tags": ["trigger:other", "ai", "integration:set"], - "triggerType": "other", - "hasAI": true, - "score": 83.93, - "scoreBreakdown": { - "traction": 0.572, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.5 - }, - "source": "https://n8n.io/workflows/3638", - "author": "jimleuk", - "success": true - }, - { - "id": 5629, - "slug": "multi-channel-workflow-error-alerts-with-telegram-gmail-mess-5629", - "name": "Multi-Channel Workflow Error Alerts with Telegram, Gmail & Messaging Apps", - "description": "The **Error Notification workflow** is designed to instantly notify you whenever any other n8n workflow encounters an error, using popular communication channels like Telegram and Gmail—with optional support for Discord, Slack, and WhatsApp. \n\n## 💡 Why Use Error Notification workflow?\n\n- **Immediate Awareness:** Get instant alerts when workflows fail, preventing unnoticed errors and downtime.\n- **Multi-Channel Flexibility:** Notify your team via Telegram, Gmail, and optionally Slack, Discord, or WhatsApp.\n- **Detailed Context:** Receive rich error information including the error message, node name, time, and execution link for quicker fixes.\n- **Easy Integration:** Built with native n8n nodes and customizable code, simple to adopt without complex setup.\n- **Open Source & Free:** Use and adapt this workflow at no cost, making professional error monitoring accessible.\n\n## ⚡ Who Is This For?\n\n- **n8n Workflow Developers:** Quickly spot and respond to automation issues in development or production.\n- **Operations Teams:** Maintain uptime and swiftly troubleshoot errors across multiple workflows.\n- **Small to Medium Businesses:** Gain professional error alerting without expensive monitoring tools.\n- **Automation Enthusiasts:** Enhance your automation reliability with real-time failure notifications.\n\n## ❓ What Problem Does It Solve?\n\nThis workflow embedd error detection and notification directly within your n8n instance. It automates the process of catching errors as they occur, compiling meaningful context, and delivering it instantly via your preferred messaging platforms. This drastically reduces your response time to issues and streamlines error management, improving your automation reliability and operational confidence.\n\n## 🔧 What This Workflow Does\n\n⏱ **Trigger:** Listens for any error generated in your n8n workflows using the n8n Error Trigger node.\n\n📎 **Step 2:** Executes a Code node that formats a detailed error message capturing workflow name, error node, description, timestamp, and an execution URL.\n\n🔍 **Step 3:** Sends the formatted error notification to multiple communication channels: Telegram and Gmail by default, plus optionally Discord, Slack, and WhatsApp (disabled by default).\n\n💌 **Step 4:** Delivers rich, parsed HTML-formatted messages to ensure error readability and immediate actionability.\n\n## 🔐 Setup Instructions\n\n1. **Import** the provided `.json` file into your **[n8n instance](https://n8n.partnerlinks.io/khaisastudio) (Cloud or self-hosted)**.\n\n2. **Set up credentials:**\n - Gmail OAuth credentials for sending emails via Gmail node\n - Telegram API credentials for Telegram notifications\n - (Optional) Discord Webhook URL credential for Discord notifications\n - (Optional) Slack Webhook credential for Slack notifications\n - (Optional) WhatsApp connection credentials (if enabled)\n\n3. **Customize** the Code node if needed to adjust the error message format or target chat IDs.\n\n4. **Update** the chat IDs and recipient details in each notification node according to your channels.\n\n5. **Test** the workflow by manually triggering an error in another workflow to verify proper notifications.\n\n## 🧩 Pre-Requirements\n\n- Active n8n instance (cloud or self-hosted) with version supporting Error Trigger node\n- Telegram bot credentials and chat ID\n- (Optional) Gmail, Discord, Slack, or WhatsApp accounts and webhook credentials if you want to use those channels\n\n## 🛠️ Customize It Further\n\n- Enable and configure additional notification nodes like Slack or WhatsApp to fit your team's communication style.\n- Customize the error message template in the Code node to include extra metadata or format it differently (e.g., markdown).\n- Integrate with incident management tools via webhook nodes or create tickets automatically on error.\n\n## 🧠 Nodes Used\n\n- Error Trigger\n- Code\n- Telegram\n- Gmail\n- Discord (disabled)\n- Slack (disabled)\n- WhatsApp (disabled)\n- Sticky Note (for description)\n\n## 📞 Support\n\n**Made by:** [khaisa Studio](http://khaisa.studio) \n**Tag:** notification,error,monitoring,workflow,automation,alerts \n**Category:** Monitoring & Alerts \n**Need a custom?** Need a custom? contact me on **[LinkedIn](https://www.linkedin.com/in/khmuhtadin/)** or **[Web](khmuhtadin.com)**", - "nodes": [ - "n8n-nodes-base.errorTrigger", - "n8n-nodes-base.code", - "n8n-nodes-base.gmail", - "n8n-nodes-base.whatsApp", - "n8n-nodes-base.slack", - "n8n-nodes-base.telegram", - "n8n-nodes-base.discord", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:other", "integration:code"], - "triggerType": "other", - "hasAI": false, - "score": 83.89, - "scoreBreakdown": { - "traction": 0.445, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 1 - }, - "source": "https://n8n.io/workflows/5629", - "author": "khmuhtadin", - "success": true - }, - { - "id": 11460, - "slug": "automate-3d-body-model-generation-from-images-using-sam-3d-g-11460", - "name": "Automate 3D Body Model Generation from Images Using SAM-3D & Google Sheets", - "description": "This workflow automates the process of **generating 3D human body models** (in `.glb` format) from **single image** using SAM-3D model. It operates by connecting a Google Sheet as a data source with the external AI processing API. \n\n| Start | Result|\n|--------|---------|\n| ![image](https://n3wstorage.b-cdn.net/n3witalia/golf-swing.jpg) | ![image](https://n3wstorage.b-cdn.net/n3witalia/golf-swing-result.png) |\n\n\n\n---\n\n### **Use Cases**\n\n#### **1. ✅ Sports Analysis & Motion Optimization**\n\n3D models allow precise analysis of posture, angles, and technique.\nPossible applications:\n\n* **Golf swing analysis**\n Identify stance, rotation, shoulder/hip alignment, and follow-through.\n* **Tennis serve biomechanics**\n Optimize shoulder rotation, racket angle, leg push-off.\n* **Running gait analysis**\n Evaluate stride symmetry, foot strike, and body tilt.\n* **Cycling posture optimization**\n Reduce drag by analyzing torso angle and hand position.\n* **Swimming technique evaluations**\n Compare ideal vs. actual joint angles.\n\n#### **2. ✅ Fitness, Health & Physiotherapy**\n\n3D models can visually highlight imbalances or incorrect positions.\n\n* **Posture correction assessments**\n Identify spinal misalignment or uneven weight distribution.\n* **Physical therapy progress tracking**\n Compare poses over time to assess recovery.\n* **Ergonomics and workplace safety**\n Evaluate whether a worker’s posture is safe during lifting or repetitive tasks.\n* **Home fitness coaching**\n Automated feedback for yoga, pilates, stretching exercises.\n\n#### **3. ✅ Fashion, Apparel & Virtual Try-On**\n\nPhotorealistic body reconstruction helps generate tailored outfits or evaluate fit.\n\n* **Virtual try-on for clothing brands**\n Produce accurate 3D avatars to test garments digitally.\n* **Custom-made fashion**\n Use 3D measurements for bespoke tailoring patterns.\n* **Model pose simulation**\n Test clothing fit in dynamic or unusual positions (e.g., dance, athletic poses).\n\n#### **4. ✅ Gaming, Animation & Digital Content Creation**\n\nQuick 3D reconstruction reduces production time for digital humans.\n\n* **Character rigging from real people**\n Generate 3D avatars ready for animation.\n* **Motion capture alternatives**\n Recreate specific poses without expensive mocap systems.\n* **VR/AR content creation**\n Deploy 3D characters into immersive environments.\n* **Comics, illustration, and concept art**\n Use 3D poses as reference models to speed up drawing.\n\n#### **5. ✅ Medical, Research & Educational Applications**\n\nHuman-body 3D models provide insights in scientific or practical contexts.\n\n* **Anthropometric measurements**\n Estimate height, limb length, body proportions from images.\n* **Posture and musculoskeletal studies**\n Analyze joint angle distribution in different poses.\n* **Rehabilitation robotics or exoskeleton design**\n Fit devices to a patient’s real body shape.\n* **Training materials for anatomy or movement science**\n Generate accurate pose examples for students.\n\n#### **6.✅ Security, Forensics & Reconstruction**\n\nWhen allowed ethically and legally, 3D models can support investigations.\n\n* **Reconstruction of accident scenes**\n Understand how a person fell, collided, or moved.\n* **Analysis of body posture in video frames**\n Useful for determining gesture patterns or mobility constraints.\n\n#### **7. ✅ Art, Photography & Creative Industries**\n\nArtists often need unusual or complex human poses.\n\n* **Pose reference creation**\n For painting, 3D sculpting, illustration, or storyboarding.\n* **Recreating dynamic action scenes**\n Parkour, martial arts, ballet, expressive dance.\n* **Virtual studio lighting tests**\n Apply simulated lighting to a 3D model before shooting.\n\n---\n\n### **How It Works**\n\nThis workflow automates the process of generating 3D human body models (in `.glb` format) from single images using the FAL.AI SAM-3D service. It operates by connecting a Google Sheet as a data source with the external AI processing API. Here is the operational flow:\n\n1. **Trigger & Data Fetch:** The workflow begins either manually (via \"Test workflow\") or on a schedule. It queries a designated Google Sheet to find rows where the \"3D RESULT\" column is empty, indicating a new image needs processing.\n2. **API Request & Queuing:** For each new image, it sends the image URL to the FAL.AI SAM-3D API endpoint (`/fal-ai/sam-3/3d-body`), which queues the job and returns a unique `request_id`.\n3. **Status Polling & Waiting:** The workflow enters a polling loop. It waits 60 seconds, then checks the job's status using the `request_id`. If the status is not \"COMPLETED\", it waits another 60 seconds and checks again.\n4. **Result Retrieval & Storage:** Once the status is \"COMPLETED\", the workflow fetches the final result, which contains the URL of the generated 3D model file (`.glb`). This file is then downloaded via an HTTP request.\n5. **Sheet Update:** Finally, the workflow updates the original Google Sheet row. It writes the URL of the generated 3D model into the \"IMAGE RESULT\" column for the corresponding `row_number`, thus marking the task as complete.\n\n\n---\n\n### **Set Up Steps**\n\nTo configure this workflow in your n8n environment, follow these steps:\n\n1. **Prepare the Google Sheet:**\n * Clone the provided Google Sheet template.\n * Insert the URLs of the model images you want to convert into the \"IMAGE MODEL\" column.\n * Leave the \"IMAGE RESULT\" column empty; it will be populated automatically.\n * In n8n, set up a \"Google Sheets OAuth2 API\" credential and connect it to the **Get new image** and **Update result** nodes. Ensure the `documentId` points to your cloned sheet.\n\n2. **Configure the FAL.AI API Connection:**\n * Create an account at [fal.ai](https://fal.ai/) and obtain your API key.\n * In n8n, create an \"HTTP Header Auth\" credential. Set the **Header Name** to `Authorization` and the **Header Value** to `Key YOUR_API_KEY_HERE` (replace with your actual key).\n * Apply this credential to the following nodes: **Create 3D Image**, **Get status**, and **Get Url 3D image**.\n\n3. **Verify Workflow Logic (Key Nodes):**\n * **Get new image:** Confirm the `filtersUI` is set to look for empty rows in the correct column (e.g., \"3D RESULT\" or \"IMAGE RESULT\").\n * **Create 3D Image:** Verify the JSON body correctly references the image URL from the previous node (`{{ $json.image }}`).\n * **Completed? (If node):** Ensure the condition checks for the string `COMPLETED` from `{{ $json.status }}`.\n * **Update result:** Double-check that the column mapping correctly uses `row_number` to match the row and updates the \"IMAGE RESULT\" column with the GLB URL \n4. **Activate & Test:**\n * Save the workflow.\n * Use the **When clicking ‘Test workflow’** node for an initial manual test with one image URL in your sheet.\n * Once confirmed working, you can enable the **Schedule Trigger** node for automatic, periodic execution.\n\n---\n\n👉 [Subscribe to my new **YouTube channel**](https://youtube.com/@n3witalia). Here I’ll share videos and Shorts with practical tutorials and **FREE templates for n8n**.\n\n[![image](https://n3wstorage.b-cdn.net/n3witalia/youtube-n8n-cover.jpg)](https://youtube.com/@n3witalia)\n\n---\n\n### **Need help customizing?** \n[Contact me](mailto:info@n3w.it) for consulting and support or add me on [Linkedin](https://www.linkedin.com/in/davideboizza/).", - "nodes": [ - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.wait", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.if", - "n8n-nodes-base.googleSheets", - "n8n-nodes-base.set" - ], - "tags": ["trigger:manual", "integration:httpRequest"], - "triggerType": "manual", - "hasAI": false, - "score": 83.87, - "scoreBreakdown": { - "traction": 0.535, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.636 - }, - "source": "https://n8n.io/workflows/11460", - "author": "n3witalia", - "success": true - }, - { - "id": 13651, - "slug": "generate-ai-trading-alerts-from-coingecko-and-alpha-vantage--13651", - "name": "Generate AI trading alerts from CoinGecko and Alpha Vantage via Slack, email and SMS", - "description": "Automates real-time market monitoring, technical analysis, AI-powered signal generation for cryptocurrencies (and stocks), filters high-confidence trades, and delivers actionable alerts via multiple channels.\n\n## Good to Know\n- Runs **every 5–30 minutes** (configurable trigger) to catch fresh market opportunities\n- Pulls **real-time price data** from multiple crypto/stock sources in parallel\n- Calculates popular **technical indicators** (RSI, MACD, Moving Averages, etc.)\n- Uses an **AI model** (likely Grok/xAI, OpenAI, or similar) to interpret indicators and generate buy/sell signals with confidence scores\n- Applies **multi-layer filtering** to reduce noise (thresholds, validation rules)\n- Stores signals in a **database**, logs execution history, and sends notifications\n- Supports **email**, **Telegram**, **Discord**, **SMS** (via Twilio), or **trading execution** webhooks\n- Saves significant time compared to manual chart watching\n\n## How It Works\n\n### 1. Trigger\n- **Schedule Trigger** or **Manual Trigger** (every 5–30 minutes)\n- Optional: **Market Hours / Kill-zone** filter (e.g. avoid low-volume periods)\n- Can be webhook-based for on-demand runs\n\n### 2. Fetch & Prepare Data\n- Fetches **real-time / recent OHLCV data** for a watchlist of cryptocurrencies (and possibly stocks)\n- Sources: CoinGecko, Binance, Alpha Vantage, CoinMarketCap, Bybit, Kraken, etc. (multiple in parallel)\n- Combines data from different APIs\n- Prepares structured dataset (candles, volume, current price)\n- Calculates **technical indicators** in parallel or via Code node / community nodes (e.g. RSI(14), MACD, EMA/SMA crossovers, Bollinger Bands, etc.)\n\n### 3. Analysis & Signal Generation\n- Sends prepared market data + calculated indicators to an **AI model**\n- Prompt instructs the model to:\n - Analyze current market structure\n - Evaluate indicator confluence\n - Generate **Buy / Sell / Hold** signal\n - Assign **confidence score** (e.g. 0–100%)\n - Provide short reasoning\n- Optional: Rule-based pre-filter (e.g. only proceed if RSI < 30 or MACD crossover)\n\n### 4. Validate, Alert & Store\n- **Filters** signals: minimum confidence threshold, no-duplicate check, max signals per run, etc.\n- **Validates** against additional rules (e.g. volume spike, no recent opposite signal)\n- **Stores** signal in database (PostgreSQL, Supabase, Airtable, Google Sheets, etc.)\n - Includes: timestamp, symbol, signal type, confidence, price, indicators snapshot, AI reasoning\n- **Logs** full execution trace\n- **Sends alerts**:\n - Email notification\n - Telegram / Discord message (with formatting)\n - SMS (Twilio)\n - Webhook to trading bot / execution system\n - Optional: Push to tradingview alert or auto-execute (paper/live)\n\n## Data Sources\n- **Market Data APIs** — CoinGecko, Binance, Alpha Vantage, CoinMarketCap, etc.\n- **Technical Indicators** — Calculated via Code node, community nodes (e.g. phoenix indicators), or external libraries\n- **AI Model** — Grok (xAI), OpenAI (GPT-4o), Claude, Gemini, or local LLM\n- **Notification Channels** — Email (Gmail/SMTP), Telegram, Discord, Twilio, webhook\n- **Storage** — Google Sheets, PostgreSQL, Supabase, Notion, Airtable\n\n## How to Use\n1. **Import** the workflow JSON into your n8n instance\n2. **Configure credentials**:\n - API keys for market data providers (Alpha Vantage, CoinGecko Pro, Binance, etc.)\n - AI provider (Grok API key, OpenAI key, etc.)\n - Notification services (Telegram bot token, email SMTP, Twilio, etc.)\n - Database connection if used\n3. **Set your watchlist** — edit the symbols in the fetch node(s)\n4. **Tune the schedule** — change interval in the trigger node\n5. **Customize AI prompt** — adjust in the AI node for more aggressive/conservative signals\n6. **Set filters** — confidence threshold, max alerts per cycle, etc.\n7. **Test manually** — use Execute Workflow button with sample data\n8. **Activate & monitor** — check Executions tab for logs\n\n## Requirements\n- n8n (self-hosted or cloud)\n- API keys for at least one market data provider\n- AI API access (Grok, OpenAI, etc.)\n- Notification credentials (Telegram bot, email account, etc.)\n- Optional: Database for persistent signal history\n\n## Customizing This Workflow\n- Add more **exchanges/sources** for better data redundancy\n- Include **on-chain metrics** (whale alerts, funding rates) via additional APIs\n- Switch AI model or fine-tune prompt for your trading style\n- Add **risk management** rules (position sizing, stop-loss levels)\n- Integrate **auto-trading** via exchange API (Binance, Bybit, Alpaca, etc.)\n- Create **dashboard** output (Google Sheets + Looker Studio / Grafana)\n- Add **backtesting mode** using historical data\n- Implement **blackout periods** or news filter to avoid high-impact events\n", - "nodes": [ - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.merge", - "n8n-nodes-base.code", - "n8n-nodes-base.filter", - "n8n-nodes-base.postgres", - "n8n-nodes-base.switch", - "n8n-nodes-base.emailSend" - ], - "tags": ["trigger:schedule", "integration:httpRequest"], - "triggerType": "schedule", - "hasAI": false, - "score": 83.1, - "scoreBreakdown": { - "traction": 0.521, - "recency": 1, - "coverage": 1, - "aiAgent": 0, - "clarity": 1, - "density": 0.533 - }, - "source": "https://n8n.io/workflows/13651", - "author": "oneclick-ai", - "success": true - }, - { - "id": 3675, - "slug": "mcp-supabase-server-for-ai-agent-with-rag-multi-tenant-crud-3675", - "name": "MCP Supabase Server for AI Agent with RAG & Multi-Tenant CRUD", - "description": "# Supabase AI Agent with RAG & Multi-Tenant CRUD \n**Version**: 1.0.0 \n**n8n Version**: 1.88.0+ \n**Author**: Koresolucoes \n**License**: MIT \n\n---\n\n## Description \nA stateful AI agent workflow powered by **Supabase** and **Retrieval-Augmented Generation (RAG)**. Enables persistent memory, dynamic CRUD operations, and multi-tenant data isolation for AI-driven applications like customer support, task orchestration, and knowledge management. \n\n**Key Features**: \n- 🧠 **RAG Integration**: Leverages OpenAI embeddings and Supabase vector search for context-aware responses. \n- 🗃️ **Full CRUD**: Manage `agent_messages`, `agent_tasks`, `agent_status`, and `agent_knowledge` in real time. \n- 📤 **Multi-Tenant Ready**: Supports per-user/organization data isolation via dynamic table names and webhooks. \n- 🔒 **Secure**: Role-based access control via Supabase Row Level Security (RLS). \n\n---\n\n## Use Cases \n1. **Customer Support Chatbots**: Persist conversation history and resolve queries using institutional knowledge. \n2. **Automated Task Management**: Track and update task statuses dynamically. \n3. **Knowledge Repositories**: Store and retrieve domain-specific information for AI agents. \n\n---\n\n## Instructions \n### 1. Import Template \n- Go to **n8n > Templates > Import from File** and upload this workflow. \n\n### 2. Configure Credentials \n- Add your **Supabase** and **OpenAI** API keys under **Settings > Credentials**. \n\n### 3. Set Up Multi-Tenancy (Optional) \n- **Dynamic Webhook Path**: \n Replace the default webhook path with `/mcp/tool/supabase/:userId` to enable per-user routing. \n- **Table Names**: \n Use a **Set Node** to dynamically generate table names (e.g., `agent_messages_{{userId}}`). \n\n### 4. Activate & Test \n- Enable the workflow and send test requests to the webhook URL. \n\n---\n\n## Tags \n`AI Agent` `RAG` `Supabase` `CRUD` `Multi-Tenant` `OpenAI` `Automation` \n\n---\n\n## Screenshots \n![Captura de tela 20250423 124013.png](fileId:1158)\n![Captura de tela 20250423 123954.png](fileId:1159)\n\n---\n\n## License \nThis template is licensed under the MIT License. ", - "nodes": [ - "@n8n/n8n-nodes-langchain.mcpTrigger", - "@n8n/n8n-nodes-langchain.vectorStoreSupabase", - "@n8n/n8n-nodes-langchain.embeddingsOpenAi", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.supabaseTool" - ], - "tags": ["trigger:other", "ai", "integration:supabaseTool"], - "triggerType": "other", - "hasAI": true, - "score": 82.03, - "scoreBreakdown": { - "traction": 0.558, - "recency": 1, - "coverage": 1, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.174 - }, - "source": "https://n8n.io/workflows/3675", - "author": "koresoluciones", - "success": true - }, - { - "id": 12400, - "slug": "handle-customer-support-queries-with-cache-first-rag-using-r-12400", - "name": "Handle customer support queries with cache-first RAG using Redis, LangCache and OpenAI", - "description": "\nAn end-to-end **Retrieval-Augmented Generation (RAG)** customer support workflow for **n8n**, using a **cache-first strategy (LangCache)** combined with a **Redis vector store** powered by **OpenAI embeddings**. \nThis template is designed for fast, accurate, and cost-efficient customer support chatbots, internal help desks, and knowledge-base assistants.\n\n---\n\n## Overview\n\nThis workflow implements a **production-ready RAG architecture** optimized for customer support use cases. Incoming chat messages are processed through a structured pipeline that prioritizes cached answers, falls back to semantic vector search when needed, and validates response quality before returning a final answer.\n\nThe workflow supports:\n- Multi-question user inputs\n- Intelligent query decomposition\n- Cache reuse to reduce latency and cost\n- High-precision retrieval from a Redis vector database\n- Quality evaluation and controlled retries\n- Final answer synthesis into a single, coherent response\n\n---\n\n## Key Features\n\n- **Chat-based RAG pipeline** using n8n’s Chat Trigger\n- **Query decomposition** for multi-topic questions\n- **LangCache integration** (search + save)\n- **Redis Vector Store** for semantic retrieval\n- **OpenAI embeddings and chat models**\n- **Quality scoring** with retry logic\n- **Session memory buffers** for contextual continuity\n- **Fallback-safe behavior** (no hallucinations)\n\n---\n\n## How the Workflow Works\n\n### 1. Chat Trigger\nThe workflow starts when a new chat message is received.\n\n### 2. Configuration Setup\nA centralized configuration node defines:\n- LangCache base URL\n- Cache ID\n- Similarity threshold (default: `0.75`)\n- Maximum retrieval iterations (default: `2`)\n\n### 3. Query Decomposition\nThe user message is analyzed and decomposed into:\n- A single focused question, or\n- Multiple independent sub-questions\n\nThis improves retrieval accuracy and cache reuse.\n\n### 4. Cache-First Retrieval\nEach sub-question is processed independently:\n- The workflow first searches **LangCache**\n- If a high-similarity cached answer is found, it is reused immediately\n\n### 5. Vector Retrieval (Cache Miss)\nIf no cache hit exists:\n- The query is embedded using OpenAI embeddings\n- A semantic search is executed against the **Redis vector index**\n- Retrieved knowledge-base documents are passed to a research-only agent\n\n### 6. Knowledge-Only Answering\nThe research agent:\n- Answers **strictly from the retrieved knowledge**\n- Returns `\"no info found\"` if no relevant data exists\n\n### 7. Quality Evaluation\nEach generated answer is evaluated by a dedicated quality-check node:\n- Outputs a numerical `SCORE` (0.0 – 1.0)\n- Provides textual feedback\n- Low scores can trigger limited retries\n\n### 8. Cache Update\nHigh-quality answers are saved back to **LangCache** for future reuse.\n\n### 9. Aggregation & Synthesis\nAll sub-answers are aggregated and synthesized into:\n- One final, user-facing response, or\n- A polite fallback message if information is insufficient\n\n---\n\n## Main Nodes & Responsibilities\n\n- **When Chat Message Received** — Entry point for user messages\n- **LangCache Config** — Centralized configuration values\n- **Decompose Query (LangChain Agent)** — Splits complex queries\n- **Structured Output Parser** — Ensures valid JSON output\n- **Search LangCache** — Cache lookup via HTTP\n- **Redis Vector Store** — Semantic retrieval from Redis\n- **Embeddings OpenAI** — Vector generation\n- **Research Agent** — KB-only answering (no hallucinations)\n- **Quality Evaluator** — Scores answer relevance\n- **Save to LangCache** — Stores validated answers\n- **Memory Buffers** — Session context handling\n- **Response Synthesizer** — Final message generation\n\n---\n\n## Setup Instructions\n\n### 1. Configure Credentials\nCreate the following credentials in n8n:\n- **OpenAI API**\n- **Redis**\n- **HTTP Bearer Auth** (for LangCache)\n\n### 2. Prepare the Knowledge Base\n- Embed your documents using OpenAI embeddings\n- Insert them into the configured Redis vector index\n- Ensure documents are concise and well-structured\n\n### 3. Configure LangCache\nUpdate the configuration node with:\n- `langcacheBaseUrl`\n- `langcacheCacheId`\n- Optional tuning for similarity threshold and iterations\n\n### 4. Test the Workflow\n- Use the example data loader or schedule trigger\n- Send test chat messages\n- Validate cache hits, vector retrieval, and final responses\n\n---\n\n## Recommended Tuning\n\n- **Similarity Threshold:** `0.7 – 0.85`\n- **Max Iterations:** `1 – 3`\n- **Quality Score Cutoff:** `0.7`\n- **Model Choice:** Use faster models for low latency, stronger models for accuracy\n- **Cache Policy:** Cache only high-confidence answers\n\n---\n\n## Security & Compliance Notes\n\n- Store API keys securely using n8n credentials\n- Avoid caching sensitive or personally identifiable information\n- Apply least-privilege access to Redis and LangCache\n- Consider logging cache writes for audit purposes\n\n---\n\n## Common Use Cases\n\n- Customer support chatbots\n- Internal help desks\n- Knowledge-base assistants\n- Self-service support portals\n- AI-powered FAQ systems\n\n---\n\n## Template Metadata (Recommended)\n\n- **Template Name:** AI Customer Support — Redis RAG (LangCache + OpenAI)\n- **Category:** Customer Support / AI / RAG\n- **Tags:** \n `customer-support`, `RAG`, `knowledge-base`, `redis`, `openai`, `langcache`, `chatbot`, `n8n-template`\n- **Difficulty Level:** Intermediate\n- **Required Integrations:** OpenAI, Redis, LangCache\n", - "nodes": [ - "@n8n/n8n-nodes-langchain.chatTrigger", - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.outputParserStructured", - "n8n-nodes-base.splitOut", - "n8n-nodes-base.httpRequest", - "n8n-nodes-base.if", - "n8n-nodes-base.set", - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.aggregate", - "@n8n/n8n-nodes-langchain.vectorStoreRedis", - "@n8n/n8n-nodes-langchain.embeddingsOpenAi", - "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.openAi", - "n8n-nodes-base.scheduleTrigger", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:chatTrigger", "ai", "integration:langchain"], - "triggerType": "chatTrigger", - "hasAI": true, - "score": 77.65, - "scoreBreakdown": { - "traction": 0.24, - "recency": 1, - "coverage": 1, - "aiAgent": 1, - "clarity": 1, - "density": 0.571 - }, - "source": "https://n8n.io/workflows/12400", - "author": "mohelwah", - "success": true - }, - { - "id": 5010, - "slug": "rag-starter-template-using-simple-vector-stores-form-trigger-5010", - "name": "RAG Starter Template using Simple Vector Stores, Form trigger and OpenAI", - "description": "This template quickly shows how to use RAG in n8n.\n\n## Who is this for?\nThis template is for everyone who wants to start giving knowledge to their Agents through RAG.\n\n## Requirements\nHave a PDF with custom knowledge that you want to provide to your agent.\n\n## Setup\nNo setup required. Just hit `Execute Workflow`, upload your knowledge document and then start chatting.\n\n## How to customize this to your needs\n1. Add custom instructions to your Agent by changing the prompts in it.\n2. Add a different way to load in knowledge to your vector store, e.g. by looking at some Google Drive files or loading knowledge from a table.\n2. Exchange the `Simple Vector Store` nodes with your own vector store tools ready for production.\n3. Add a more sophisticated way to rank files found in the vector store.\n\nFor more information read our [docs on RAG in n8n](https://docs.n8n.io/advanced-ai/rag-in-n8n/).\n\n", - "nodes": [ - "n8n-nodes-base.formTrigger", - "@n8n/n8n-nodes-langchain.embeddingsOpenAi", - "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", - "n8n-nodes-base.stickyNote", - "@n8n/n8n-nodes-langchain.vectorStoreInMemory", - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.chatTrigger", - "@n8n/n8n-nodes-langchain.lmChatOpenAi" - ], - "tags": ["trigger:formTrigger", "ai", "integration:langchain"], - "triggerType": "formTrigger", - "hasAI": true, - "score": 72.06, - "scoreBreakdown": { - "traction": 0.759, - "recency": 1, - "coverage": 0.5, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.875 - }, - "source": "https://n8n.io/workflows/5010", - "author": "n8n-team", - "success": true - }, - { - "id": 3647, - "slug": "transform-google-drive-documents-into-vector-embeddings-3647", - "name": "📥 Transform Google Drive Documents into Vector Embeddings", - "description": "Automatically convert documents from Google Drive into vector embeddings using OpenAI, LangChain, and PGVector — fully automated through n8n.\n\n---\n\n### ⚙️ What It Does\n\nThis workflow monitors a Google Drive folder for new files, supports multiple file types (PDF, TXT, JSON), and processes them into vector embeddings using OpenAI’s `text-embedding-3-small` model. These embeddings are stored in a Postgres database using the PGVector extension, making them query-ready for semantic search or RAG-based AI agents.\n\nAfter successful processing, files are moved to a separate “vectorized” folder to avoid duplication.\n\n---\n\n### 💡 Use Cases\n\n- Powering Retrieval-Augmented Generation (RAG) AI agents \n- Semantic search across private documents \n- AI assistant knowledge ingestion \n- Automated document pipelines for indexing or classification \n\n---\n\n### 🧠 Workflow Highlights\n\n- **Trigger Options:** Manual or Scheduled (3 AM daily by default) \n- **Supported File Types:** PDF, TXT, JSON \n- **Embedding Stack:** LangChain Text Splitter, OpenAI Embeddings, PGVector \n- **Deduplication:** Files are moved after processing \n- **License:** CC BY-SA 4.0 \n- **Author:** [AlexK1919](https://www.alexk1919.com)\n\n---\n\n### 🛠 What You’ll Need\n\n- **Google Drive OAuth2** credentials (connected to `Search Folder`, `Download File`, and `Move File` nodes) \n- **OpenAI API Key** (used in the `Embeddings OpenAI` node) \n- **Postgres + PGVector** database (connected in the `Postgres PGVector Store` node)\n\n---\n\n### 🔧 Step-by-Step Setup Instructions\n\n1. **Create Google OAuth2 credentials** in n8n and connect them to all Google Drive nodes.\n2. **Set your source folder** ID in the `Search Folder` node — this is where incoming files are placed.\n3. **Set your processed folder** ID in the `Move File` node — files will be moved here after vectorization.\n4. **Ensure you have a PGVector-enabled Postgres instance** and input the table name and collection in the `Postgres PGVector Store` node.\n5. **Add your OpenAI credentials** to the `Embeddings OpenAI` node and select `text-embedding-3-small`.\n6. **Optional:** Activate the `Schedule Trigger` node to run daily or configure your own schedule.\n7. **Run manually** by triggering `When clicking ‘Test workflow’` for on-demand ingestion.\n\n---\n\n### 🧩 Customization Tips\n\nWant to support more file types or enhance the pipeline?\n\n- **Add new extractors**: Use `Extract from File` with other formats like DOCX, Markdown, or HTML.\n- **Refine logic by file type**: The `Switch` node routes files to the correct extraction method based on MIME type (`application/pdf`, `text/plain`, `application/json`).\n- **Pre-process with OCR**: Add an OCR step before extraction to handle scanned PDFs or images.\n- **Add filters**: Enhance the `Search Folder` or `Switch` node logic to skip specific files or folders.\n\n---\n\n### 📄 License\n\nThis workflow is available under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) license. You are free to use, adapt, and share this workflow for non-commercial purposes under the terms of this license.\n\nFull license details: https://creativecommons.org/licenses/by-nc-sa/4.0/", - "nodes": [ - "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", - "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter", - "@n8n/n8n-nodes-langchain.vectorStorePGVector", - "n8n-nodes-base.manualTrigger", - "n8n-nodes-base.splitInBatches", - "n8n-nodes-base.googleDrive", - "n8n-nodes-base.scheduleTrigger", - "n8n-nodes-base.stickyNote", - "n8n-nodes-base.switch", - "n8n-nodes-base.extractFromFile", - "@n8n/n8n-nodes-langchain.embeddingsOpenAi" - ], - "tags": ["trigger:manual", "ai", "integration:langchain"], - "triggerType": "manual", - "hasAI": true, - "score": 66.66, - "scoreBreakdown": { - "traction": 0.529, - "recency": 1, - "coverage": 0.5, - "aiAgent": 0.6, - "clarity": 1, - "density": 0.714 - }, - "source": "https://n8n.io/workflows/3647", - "author": "alexk1919", - "success": true - }, - { - "id": 3848, - "slug": "build-a-document-qa-system-with-rag-using-milvus-cohere-and--3848", - "name": "Build a Document QA System with RAG using Milvus, Cohere, and OpenAI for Google Drive", - "description": "\n\n### Template Description\n\nThis template creates a powerful Retrieval Augmented Generation (RAG) AI agent workflow in [n8n](https://n8n.partnerlinks.io/5xf5bs8y3ruv). It monitors a specified Google Drive folder for new PDF files, extracts their content, generates vector embeddings using Cohere, and stores these embeddings in a Milvus vector database. Subsequently, it enables a RAG agent that can retrieve relevant information from the Milvus database based on user queries and generate responses using OpenAI, enhanced by the retrieved context.\n\n### Functionality\n\nThe workflow automates the process of ingesting documents into a vector database for use with a RAG system.\n\n 1. **Watch New Files:** Triggers when a new file (specifically targeting PDFs) is added to a designated Google Drive folder.\n\n 2. **Download New:** Downloads the newly added file from Google Drive.\n\n 3. **Extract from File:** Extracts text content from the downloaded PDF file.\n\n 4. **Default Data Loader / Set Chunks:** Processes the extracted text, splitting it into manageable chunks for embedding.\n\n 5. **Embeddings Cohere:** Generates vector embeddings for each text chunk using the Cohere API.\n\n 6. **Insert into Milvus:** Inserts the generated vector embeddings and associated metadata into a Milvus vector database.\n\n 7. **When chat message received:** Adapt the trigger tool to fit your needs. \n 8. **RAG Agent:** Orchestrates the RAG process.\n\n 9. **Retrieve from Milvus:** Queries the Milvus database with the user's chat query to find the most relevant chunks.\n\n10. **Memory:** Manages conversation history for the RAG agent to optimize cost and response speed.\n\n11. **OpenAI / Cohere embeddings:** Uses ChatGPT 4o for text generation.\n\n### Requirements\n\nTo use this template, you will need:\n\n* An [n8n instance](https://n8n.partnerlinks.io/5xf5bs8y3ruv) (cloud or self-hosted).\n\n* Access to a Google Drive account to monitor a folder.\n\n* A Milvus instance or access to a Milvus cloud service like [Zilliz](https://zilliz.com). \n\n* A [Cohere](https://cohere.com) API key for generating embeddings.\n\n* An OpenAI API key for the RAG agent's text generation.\n\n### Usage\n\n1. Set up the required credentials in n8n for Google Drive, Milvus, Cohere, and OpenAI.\n\n2. Configure the \"Watch New Files\" node to point to the Google Drive folder you want to monitor for PDFs.\n\n3. Ensure your Milvus instance is running and the target cluster is set up correctly.\n\n4. Activate the workflow.\n\n5. Add PDF files to the monitored Google Drive folder. The workflow will automatically process them and insert their embeddings into Milvus.\n\n6. Interact with the RAG agent. The agent will use the data in Milvus to provide context-aware answers.\n\n### Benefits\n\n* Automates document ingestion for RAG applications.\n\n* Leverages Milvus for high-performance vector storage and search.\n\n* Uses Cohere for generating high-quality text embeddings.\n\n* Enables building a context-aware AI agent using your own documents.\n\n### Suggested improvements\n* **Support for More File Types:** Extend the \"Watch New Files\" node and subsequent extraction steps to handle various document types (e.g., .docx, .txt, .csv, web pages) in addition to PDFs.\n\n* **Error Handling and Notifications:** Implement robust error handling for each step of the workflow (e.g., failed downloads, extraction errors, Milvus insertion failures) and add notification mechanisms (e.g., email, Slack) to alert the user.\n\n### Get in touch with us\n\nContact us at [https://1node.ai](https://1node.ai) ", - "nodes": [ - "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", - "@n8n/n8n-nodes-langchain.embeddingsCohere", - "@n8n/n8n-nodes-langchain.chatTrigger", - "n8n-nodes-base.googleDriveTrigger", - "n8n-nodes-base.googleDrive", - "@n8n/n8n-nodes-langchain.vectorStoreMilvus", - "@n8n/n8n-nodes-langchain.agent", - "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter", - "n8n-nodes-base.extractFromFile", - "n8n-nodes-base.stickyNote" - ], - "tags": ["trigger:chatTrigger", "ai", "integration:langchain"], - "triggerType": "chatTrigger", - "hasAI": true, - "score": 65.61, - "scoreBreakdown": { - "traction": 0.444, - "recency": 1, - "coverage": 0.5, - "aiAgent": 1, - "clarity": 1, - "density": 0.846 - }, - "source": "https://n8n.io/workflows/3848", - "author": "aitoralonso", - "success": true - } - ] -} diff --git a/packages/@n8n/workflow-sdk/examples/templates.zip b/packages/@n8n/workflow-sdk/examples/templates.zip deleted file mode 100644 index ec29d1240b0f073708ef8ac8822b3ee012b69ce1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 533128 zcmY(Lb8zNR*!H`%ZF}3Tx3+C>ZR4qJ+qP}n=2Kf++t#-)zj^;T$;_S1Bs0m$bzQ&v zL_r!1932D%8X6=ns8sv?PWSl_5(r2&KM05z2ndLQ1)YJj6Trs6$;6n>+=0#&;9zBH z4RED11vtpwcf5d%%f&>)wo)Cbm11(ih>J@>05e<-rHnq0ypbEdeq|MJiuZ`p zGl3wFkTyY7vPmq5f1jxO1T-!dDLq_-^mX(~F1B%7da+Zs4htb`d;3aGJ`N`h05uwh zt~nD18M56Y-uk;&PRTtIHN9|oaezXSGtv* z&F|vsVi{9^9@S-hun)YYLX`NsJJ$zxF&r3w-Elic)b^hHxNf--{j!K$0Wn?rlAMFTsM( zMKY);qmADd9!Um;iXR-#4^8EkkHo5&kceYG7M-CsoSicTkN3W3Tl-;LyvNm?pzEi9 za#OK_>p4hb0*A`Y8)yWU{i=?T_G)?s6HdZ9Gy~7)Ijx_93agy{dyHGm@uhl~lX0@< z8TGWw&+^8$HgnDdf?*0;UaKgjm_rjs2gS(w1;&A#Cpg-WCC}Ed0h=O8sAT5`9o{6+ zsW!L1NIg>V>$7X)V|Hs`vxZo`n8e*1(28rh@oWc8`y+PO4-p!X6n8Id4kf1B06S_E zJ1f4?a7SFvNv=#xlHTk@w(x-RKE&ZIQ+q};F;!-hfHw1mOjgx2E-^x4mRQL35Kw4<{C96G9Lgu_<%?Lu|EM&eh-WV; zseyj~PLCAz9nMaPl+sD6?ZnXlf3l{$E?Fl&Ph`9sivmHfY47oq$T#o%MOAW+T@SdW z0{|e5O!zKoTcmYQ9?~wPGEOQLRJZX@g2v+`qd@mylPfkSYQu`mhflTV%)A-d)6cu} zhB!#oM-lhz(~(&;eX#_-2+R$L62UxR;zKS&>!Ar#{P&buG%d0kkSXCfT9B_n*S0D#c%ns$0HQOBGm zHV?%qQ}SFqjN^tel>DInC}j~z#JJ}X*2d#cbrMlx!%@$(S- zd2eOp`@L6Cb#;lZO(?5>=Kwi4$qa5c!v!x|)Pgf<()YD6rbv_bjS!fzV$BO9Y%8psfHd#qXn5>Og51I62qsl8 zJ_rIe4jioKzNy0s8AtBgizBaOy3K}Jq0WmHG1q?<&KGbs0era_)Q;;U{Y7H}%TG;6 zEv`LZlP;&aO-G9|gbFo2*#!w2P787E)lb8&&GHFy=)3eo@xN5>s<2p8+ z$E{;pA-q#KiaNnG6rm(O*<8Q1dF4&M%ZLo`&Ol2-w}?old{~z=vjZz1Uz@vBS+7#a zFPB81_=TS53fQ^KcXi}tJuTHg5qA)t5blWG2F*ZSn!uC^(FctJO_bSqrSAP<(vv%` zEl+}V@+Z-$=aTyTIdH}?r7Z`HWY8Wx%1)pQB_=dHRwouwh##KUmr=XEoTwtaNkG6-0i_^o=$K@;S5LaoeM2U9N_jd2$ zVfXV^Z^PW#*jyUiQ6U{2R*l{lGJ1sw5s5x_b{0{NkYs*vbU)xO_P7?fu^%~k{~XJd zUlZuKmIYkvAKtT{thy4c@%~^a!_Udp!SChs06bo@VEzjJTlI@Ht+`X0JuIg{IW683 zw5s`Cd`NsmH6`J7owd;UTSB5j-$qoY>yN<@F^On}e0a&iV{=lN`ij}n7lS)UznLK11B0u)JBi3K-1)f#)ue>HTX&8SV zS#MMI?woLLOAL)B7NeD9QS=Aap5^Y-QMn+V2vy?GkW!QY70xBPSJ%vvs?M#K%A6bO z8i+v}IE#E6uE}q_UcQb;J=46$KX;~wW=X#uZ;I^XechbNUsCM!4o}Z?bLyF=bnNdv(yJkd9ld7 zA(^n1TcV;tM|}e3a`S55`8tIh{8SH%++%KVv-5{o(o6s;X<5mlL`W{{<*LXA?JSr# z19kh5HiJG=)W3?T!5q;0*rRd>d&Vbji;v|*T$RJ=aM@TYSe%SbwhWOc1mmg;`M0~$ z67GYvE41d$OSU1ZzN5UwnRkSm&lpo-R1#Z-zG?_0?yj%_PD_Y%p*+q>t-mpZ9MpijHlOFXV z?x&KPJ@}C&Z*|&w)!Sf*m1e^pN9yYC9?S|6GAY{~mGbJDTFwNpQ))}%QAMY4cid3@5<4gF>k7stRNBx^ zWS8g%qygGcOzvP~q9sE%=dxjP>TVIf#0?YAtpU(B?sA+|bn!5!?d~QT`No(n;r5W^ z5J`e^rM};I6YUdz^VC0EBCJKM%2lY1ojEXnv*LqeJ2c7^u$Brim%k#^qyKtSHbh@R z{ZtdMRJ|^|B6VucKfE`&{4(KF1Uw~`-K&*Zm{8(*RWb{J!i5S7fK8#A`GNgyOKXdX zl|*I4geSlh**=SeKA2F0&_k2WDP9-R( zcV*iuW`{_3Z%G~VzQ@5~AkEvTKUx@H*V}>bd0Np3vsHVL(qCTMMgF)<!H%ZNg?>izZCl7AqB(-|v*H_2+j>}T|M^u2) zkwP|VztV!~O=Z+Mrg&!{m2C`wx$70YHFj{F{5VwIvi1X}|Jrn132Yax`GdRyIUSik zNetY?6ic>Xq+$x?$r>xb{kjU~kS%C62RvYX3q&hN>#NVXfLQXz{3Mivcn?IpkZOP_ zNO*^+J)USdlYP^BBF58y?N;AzCuW7)8dtxf2;;@rl8xhri|zA{i27|0<1Z}6lM|g| z0W1;TvSziSX>5|nI*X2j%@gEsG!qD3BwaoHU%|f|f&r&?*!#o}JUqDdAV{AiI3nYI zltH#5^PZq7HP3daKmpD_p$(J1@JWzRcycMz+DFs!`f8W!0`evqpsXhu+`wv)sGl$g zC3;Jc*3A?(!bIAs>n`C=U9l@VHEj9O$E8SIS`QQxS(PBsPss_0<7y8PgtcebxcKGt(&SZrX zQB7|9Xv8l&pWJd(9n>}|*Mem{?3sgv@JvXu?7P~QjE^Mh_pbOV75f??(E4?MP`$W$ zb3a>Z)uANqnUSK???A)a$@P^>HDB^`csZRc)#_F6(!a8ZkmjT&xw%vzlQxgDWDgEF zK`hUHpTRc+0L-jS=p4;WO#W3&Yz!={>F8J(8Cn0!yYp<= z;)$os&a0!Jh+crxb}%fl{}2>YM2$t>(Vv8ANQqZb&!(iYRAbVS9SXoW=F*D0Pj91Q~!71Kj;JIAkBte{-Wx ze2PA`dP_|h7!;7wH)lk`;Fl}KqTI1~B9(;pU=4Zq;uJL8N;Kxg zTCGevqmV{b+qk`>f=wO&VHdiakB7s})y)nF(w9w(_(|S}?q~3K7+g6PY8B4?>=s^$ z@WnJRkisiAPoz|tE5xHqd+MSsilSw7H^~xPcEaHa)F(ZV{ydzySd==6MZZn(FGBpW z$4urM8QlrG6cv0D=0IS7*Vyz(Zm(zP*J;1Ea)TeXT|f1BIy<2Mti$3GK7f#{ z3-lT034QW~0wa;8qA$-sJ$GFJT%5QJw2{>!J!3<=!HnA>I|PJZi=}}^Gbu2HfiX-3 z_|@#`-qLWT15tSu@$Xmc zM~Ud<(=!Q{g-s)VkYOQ|65_+tTky3O?rVgH4=9ri=M}8lUKv8z#TDndL&2ITa^!?x z+5B;!FE#4lSiqZTJ_ky218xkSN4kMH`!&~T0)D9RV0`uiz|X8QFTP|oFKNx0$Gk5eolmbu&;YhS{M8cAdz+Lm$8y|L zWpiK*0<-sbs>N{8|Fj4ak;AZ)>=lagLa2ut$RQ@=XB@v%FW*tD9tDSM%q*d|Xduy! zZnlp1Jv`Rz&(v=oh8#CNKF`<7S>LCe{pdQY<>fIyk z)rd>IXJg?V>S_KcXPFn2aD1n|-Kie!1CfC-P?1yUXz_utuh_*$ROQP{)FrsG@;I+t zK4~~pHK3`p{Mn!#z{JMS_kIy|7FjsU$DLZ+6lbI&2Lh8`USNnRqU;PKyy)it#9cWS z`dgK_;|!B9TRKYj)KV8pO@u{F((; z;NZV|Nn1KkS{r(D+)htNpB~53D$sI5lg4n!+5mPYj)F=E0fPTzft z30w`rOu~VzCz5CGeAgEC=a4BsNcoKg!u&3{K&?`$WCO(|qmcgdL164{IVH`9i$%Br zkPGQi(J7+2vtg?f-`pPPu*lJ<49tkhx57Tpzi!!R_XxoVB3i_s8vt&^;YFZY41-V4 zp||BdEv8iBy4Aue3Nihbu}aYk=Mkr-wP)N5JM1w^!US!A)!IhE3ZI+AVT^}qnZR5w zHLvN%zu|$Z$Ty|B7X_QAn2IvamUa!2D`tF~Ec>kqcV=PwXSskCGnE)+%dd}gnb-Y~ zfk`Ru_K%*4?biVjIWOKPArE?~j3a#t1$lFl1GgaElwf_6o|C@r@h=6*)K|5guz317 zBQCbTfRYnVBcb19(WM%lf&S4BF;s?PeNw??TbSjb)8>k>Y_k^rrilY_SN{I++w-Ti zp(I>HSya^i$*Mq(;QJp1$=bygv`Y`lp0(T1-bNVm)CVso+CCe0fO2vt7C#Jin{SKV zOC@V3`dChJhyy!GlWqZedXj4^X1)$zNr{vpIZU`Gx9gwXGrM#)}IJW@g-r3rUz*+B)MT86d1a zg@m|qX&JckVVIH6y0nmY z3!RVnY;%B+Iqp%S%xxao!39m-^N8{ES)BY#1BU6Jl_18HA(6BV9SYVV48%;Y5=~?_ zQrBMB<~n>&+bB3lqC;q48rWrlpy7sc!0-|=@7#Ydj*(t15GHllPK*keHT{KhzWIaF zR6TGIE0N2nIQrvgh~96!=W*hy(d=hUpsqV5hteDnC_ zW0)#BPCaS$A00t1#j}ACBQORSoq_PkY`^w21uu`KW6CT|Zj*E9wJXJ$&nQmK_n-6LLAdMVp=sWhwtYPHoy0y1 z!x#5AW=1>?e0B>JBNcU_u){a5xH@>)1S07#UW`lE{aIf!;^Q|L$?&Nun`X~^(~&W! zKifXOGRm2=xQkVRxE((AeE0uup!oTP9$dO04a~=nZD%g|`4TRt;`1n1VVwB7#^U?+ z$j&I!pXsCk#8z^B`p4t1LJUJ(XBoIWt#Ef*EJ|ML(Y)SOivTzvGX*OPcVj#K(&Fk! z4arsPWi+_@kJlcQPlrQ7`!05ecNH}Oov>`+aGWR%rKE@*{wV&a!GXXD_Wj_pidt@q zQ0&s5Iy`)T8iRS~Oeq>kwdCcN%||+jQrMK9V9~*rNh?z+hdDARJdKgFD2iLI3+^!z znhou}y!YXkW(GkuzB|i_^So}947i>}G&bEozrDrfAZ28gNi51?t+dFAs=O}r8)QAs zRb)Y@qQUy=g>fkTC+#yrMt-?#$e0f`^qm*XmHqa#b?v=;YHh3#gZD|tbGepj)8#Y_ z4wIG-Ur6Zb>CK<|rDtRkC=~T`l3=-M0U=}1&H)2_vy4M6?X>)@@Lma>v*(-xr6l~^ zAc3-6@?8->F%Cj8MN$8j&b?gmr)rJQ;uPLe7W2ouewQlEbNovUTePp;c3_Ylv=>w; zxgAqD^|1Y4rYyH0%BVJ4>1=-V>&r_fo5Nndog=kFhu+?fF>U^F4rfNm{^6iI=+bhm z{ngv~F$8>m_?HazThI}rO1|CpkzVLRoo)yl|6Mn>Ni0=cIs3%Ys*g-A-y&I$k)tSU zpV=aQwL3Ag2MY6k!OWgWuQ}s;xR+tdT(ze~N+@H?^vMf5+h^2<@u5uoHA~a)(p;Yk zp>(Kc_B%%2+c(Q36Rh0HiDnx`E=#se+{hY{8H`pJoddS74jT$1zI|DOLiTf)hs;F? zAXw&U)3{I~tM+imTHAD2=40+xPIJ&sWztRN0m=eLSRrvly%964B>FCgJt=;O;>mCp zgN-+bfm~c8w`9X+Uf8 zXNUvuMhcMXi6_*Ee+*RSFUt@N7!h7?l+sZ=%UKDB=jFbOXRGB!(XZZ|nF&$X0F*a; z+`mq=GAoESn*E=d?QGF|IXFDEJ9e*f8KNYS_A)$BXoR91Cb|(uXTKRH=BJQW)>b3d zp2Ze63*hK$StS8D8{O>?HLS`+rl6Yfp+f4d9G-vUPwqrNP%{8Y>Rw{^CM}k0i$ZX2 zhOj|Dn}X_pRe5^69lXq~uv7BjF7h|U8yJ1^$trXgBQZ=T5QooURG8PlY zl8R*|xo=!)P6L)6-0q#1&=yxQxedZ51&*I zgPb}|uNS9}IKQT`CamGs#7HZx95kW2(MYg=))&5Kd@(a0i=@+f!AzD_{8id-yx1;M zQ0Jd`WeGMU67AA%rp;_Rnq;YvJ4FP0Bq{zy|3F(B`N-j<(5IpUdW}>2doYkJX-<1^ zL{1)1lW9M8!UMpAUwJZ6@qwvjxnXXe3ur6EYyl{q7n1Zq#Ki_aC`o_muGR?NC<3b} z?%4Ys5el@(I(`g__ctxeyAC|0K-=IyDk5uEOn$>6Tqyv(P{-u>_EHys~2(8Dfn}xGiE2QFB=-2r*$G_*KzP{U%vrsI#MwIariTlY5 z(M$iugMB9c`{4QF2A|VwlS*}w5FQm1c%Y0nKV6QhyZFPck2%6!-7nMwQpIMGXUjPq zK`C$b6L##GRm}F_L7uNjT!Yr1b#hhsoDP+H{Th(%BpnlJc{8E6`2J<~MWW>>)z`^u zBg%Z-=a7pEY)0OhVGq7F@EO&V5WaZZIK~;SeGnN}|qXJ=_O*}FE zGmG}#^}Y*zuI|28dSiaIfoOz?c@!LR0+{QZF9ICwe~7n0xEA7VH^tUGbV&QE438ft zq+R{l&&!(B58^Ty3LG(9-OiADJe5bF+G2ZHT8SngY`yG}!GKsD(K5YT-gp1u)kGG) zh9^v!^9BILM;zpzX}k`Z=;0Qd{zz zeg^T2I1rQ^3=U3N2~H=#IMREK?4B&wjHh42PzuN}< zu+!qT%0bOGJt_Zf(yW%sLQ#o9FwX;1Q9J!0&o)`vsaK9$gUwQLPTjtQ*KATvs9}Du zRxlHn4@wWAP12&znTP&cRfWe`*Y;u@Aqj2t-Xeq`Oj*XSodY6uf5_EapBxT*%YbG~ zr;WtrX*a}f!Q%Fj6I)YNmq|qQ@RyEea;(3Od0}O_$Uu$zELrcrJ*t#72i`I>0O-56nQDpKrOlBZ){_tAb;P(gbi3989H zw{?TWJ7f8s0Pwg>HWL@A!X&q%XM@#=Qu(v4BE5yJVZRfbnVDHrj9pF{VNe#{*?dsN z5$Z@*uJmH{)f@KF8L>oVjIC7DOl`xdzG^>r`?pH!_yzrCSsea0VoaRlDonP`+=cAm zHJ;WL#O(6J+m`JPJ+c6d^XBMu5 zZueL+EZEAog%&s&3rs9TwgWy^@=91E(cY~C+?x)-nWs`DZp%^$2AHo_;-zkf_Me&C z5gtUKnmZ$&{Vj+6n_IimpV}LrwiFD$R7DqFoHO}8#e9k`s%5|f`I%W z1p#6EuSO9I8v`>Fy8n)x{br8ip&fvoo*1`)RaVZhFONUY1lKV2wYtW)1CG2udNR@Ej26JFL2dX zhDW5%VA(Fmzoqfg)QD-h`;3M#Gi9T883&>$MYNOVb%D>+=3L^(87Cz@cuw~LD>ps< zn%6)0>{{)1LiGxzgrCYuRET6Lukpp6_GgqZ zF{ntJLypQR+l<@;kGyQH+x67X9M0lYQP1m{Snal1KL#yu4>_h1Kok3Zu1*u%FD$|t zhF#aOH^fB)XHf2JA^Pw<-$96Vu|?hS^+AsI<3`|1nF?3$?*^s=$frNNp!^zfyCDZs z_&1|L8J_f!OPxrtg!K)9ZOL?)OKh(_X5bK+SSEJrAB{n7}nJ+z8;C$8={}d<=dHf9!}8nLHFoapJ(_K1kTEL1 zk3Jd}zky}cBJM{>kTESKGUa&9H6P#(V#R!e+BfC-iSVNP+I5>X8;)vnyOsV4vYYow zU*DHg;jL!3w>ct!vetZNqET-Kg;6aJZp& z7I;PN{kS@32I6wGBx~+^(_`_cyGy0GSV@j{}tV-yqE)jQJm44B2uVN z&R-T2oTtI4H`-Vo2eNNC9jwnH6}^E?Q>JR4M!46bW!R~ZgqM6f7vcvO$N6P}{BuIv zpi|rQ?me3yEvMD*iRWXt7QgspA%v8nrUKVN?kW&)y2P~etmvh!TqH0h0yP&+fs2e{ zwNk{a#MLQbQ&CSc*^1IK8li?|-QMgNmF0h`ZEI(2QQ)KVpg)o8)DKV)g)#SNpJYNZ zhFA;;@4>#sqDs-vJf#|;!YDfV2d;0G^kBQpC+;A9swE7%u%Cg_-`*C+ONN5}d z>1sgb9p`1ybsra4K0tUaZYY{^!<<0~g@>f8;IlETW^|=%I89DwzE~e%!CO|iEsaud zK>TGzRU%VTl~Rj9LlACY>bOt2UtoVuWXmK>Rk>D0P4TRew}`q2!r7k`!_0hg3|hp4 z>n35^#_vd!vzDq$KXfkcZrM0CZ&|Qi*$Af}^xt34hahEXR7ME7jv>qX8+_<(%Fn4$ z9en|M1Z(|4dqX-dhn#Wqj-}WdIZ4ZCdQhO%BJXx%HiM*on(!g{Jl`08V&p9Tn7!4k9 z=E5whm?Od>i+L-qwgjQHC?NYw49{WEjy|6N_xw>Z`*lIKoZ@Y?DiZS=iM8Q%tL{yN zQ$?J%7BWAlWW5{6Lp12Iy_{kcGOe9zMm#cHyDgDEPsEw*#mioQ+b-)=%z~ISl6%nN z6>^`}c82ObbQMaPYXNv%f1)HmkZlT%dbdB$b3sfec<4 z3vGhWiXT_K2a3p7=puS6x=*ZEJy!^NV_zW0`J#*Ef>!jV!Zy1q+8+`uI3EwY6m-Hm zL(?1c`X>%+FBbfa1+XeMhUBeIie%VScsE5zur)Xs^}6vIkpm14YHU_tI=Hb6;BUy9!1UCR4P1X9j2W-tBiG8ds~_@ z0`umLZCjFGkZQ%k1Qk&c$dW2i-b$CPpCzshrsFGyYI9;M?#ACsuQ>4h`+T!D?72V< zpvKD-T##d>458-2{0V{z-zE?P;?1dF3WHm~0K(<4<$TD)e(7^`6|Cm$x?@?XDXm1C z-6DkguC+i#9mgzh@D5$$%pVv!A)BDpCz0HI;3`=S%9Lm4n|P|5vC=Ttwn_ z{iC9?T>nc&?EtPO4*&X@ZJe#0Ea)5oMi&2Msm(t+YVev)`Hin#g|+%561O zjb-C0+EuwFp2`peAEzSFfXRxrl0|QVzuk`NfJjtU}|mcVgxC zum^1*s z8P?<9xTgZ2fdE3(Kbf~rNIsAKGLG+ph(%_no=i(XHuV8)stU>WdC>HsZrScWt#)%UOKP)9 zo_+!&M^HM3pA4C&bllE4KhWRIsG88I-INffjBtlpp3n$A_~5)4A_n}~^{1Zm2DxF5 zkrBD;f=;a_h}F&lx~R?hNVK51UH!6D;4Zw-U&Wfb%N+4`e~rE(nnB4}8Or!T_on1c z)2D?u2oYQm5wzM4_3H%-Cou9AIqllRhGV^Exr0pzGZ4<(rXqg$@Pwt>xzQXz+z_~j zK3K5Y6fe$!6eHRuulIQdI_Y`62z)(dtKB(e%O>gP$#a`s!Z(!|nKM%$NHqG0)zs2d zTO2BwcPgJh9DZ$|XS4qd-8k;O$i>g;-CBFMcwd^?e(itSoBP^+ovCc}K%tuk$B3AV zn8sArP?It!=~S6#GA5Q~ezlUx6FWFeb?RNFy%N6rQPQ9!YTgIB99#o6xcXe+gYG`q zPyFzQ!8j|#pma1u`j^scMquV?-s|c#pDjuO=0yksY#hL&Ifu6&4G&@t7@ZU zxaE_Dg~wF1;(zSU5NbKMIoQ7{+M}gPi+Ng8%ZdQUQp%=U)&k6nt)W@?NjfoAue8a^ zs#<*!q`YkAg~t$Ww>TQ#kSD~@UzzMTFvOG-)xoL8ieSzf6sP(x%$K^bB2ksFap9{b zQ5;)v+9Xb(Sdcz+qV<+Vq|NHo6Tji`Q8i0UQ*qf| z9heI5JUQn2VpVy;a4JTu`g`LVxf;Y_JmlSbR?iqnfieqUxWB8{FBwReyS~0z!L_<~ z3Pl`!XD!jD%yZy4w^cW7$4F36dqP!onoX7AGod+s*4*kACVt`mTErf6aIDh(DJM_K6TIf)L#US>GQ=&&N>mUoaj4VeCN{2-Kef9pH zR41r6S+$@z(wBniSF*RjQFPuhoO00W> z2dl>LWER3WK9S3-3h;vS`#PeK#d9apBhf*-U4+ihn$f_#9Rm1|GPLJZ)UZMx49KmS z%l5z#i$;w|cVYfEZonS{I2cV}Jy(S!nI*f}4V39d6oL^E=}sZKAp>mUYT>_!hFHcN z74bdlanOW3W#F5@Po)v-C&MnP*mGYo)q=Y)*1UkdY30b;})SQ30P@skqL=eZ;shIf+J^F~+5CNsqmN#v=)Kymg|x?-R}Na}lXo4_!{UMzb>@@)KyTxH z5>T_o;lt*)jUc2YT*pcB5Z$4#?>|9mlqLYG5+B=G&S%$|O_`D@(|2{J;r&@K+9747 z(gT*s(bDy?!bn3^hA>5b|DcN+&dYyPGaiV`!X%ysr_Bqp>=*PE2PTZVkaW!||0>^? zl_*3uPEaJluTsuCm36=Jgcvu|{)8cZXR#(**?5;Tou zUBPHlr5nCZ@gg8CK*~(5@iy0jX_yR*EUFFMFmLPuHF6$3X^9kt1Wnc05o%G_R8dDL zUQUwrQVOHXP#LbNCjhu8;7_F!P(zNmaWE#cAhsDOR3?OdD?a%TSh9^#(1G7TN+bl} zXO3r8sZ}zQs0xat2*-nHCAp-+THG++sHFY!Q(=?(JCAI+l#NaC17VCZ-i4B`-A;VP zfzqSfK9VyPHzG6o6-yO0Vp$+!LW`6*q3}$=09yI=p#m8a7CbYdZBeM8z@aTj<C+^hAJYeq+D^+!%4>oYbHB~ zsmyX;DbGd%n4*{uqhK?;!inD51u_A+r4=usN~eZfb+SEzDyC-}kIxUb?+}!%Qj$|a zM13B*l(6XD2uGK(C$<(+M9Md>@?pVk=JS1fiOuEr{)nB;<@-86mM8iS_xgIc<>&ub z(EE=<_1pXGe{5UNu6y^{wavFgfRrcm?A0ab;lHZF7ns##(SOwOS(A<=szm*q@hD(hOX(W% zFBX6cK-^%Sa7J(F7ZqX14ZS_Wq!+4?P!k7B|3XMt;AL{Ef?-XOjO!}7onA(KH8owv zC|;=`{uv5rj%8ea?HUtwatf3 z%pN9+2CR*oK?PK^^HQaXNw9XoZ%%?RfEun;>FSph*-e>o8DE**#7KNcY_vJ)SW_9< zd(4z{qvV;`QACY7n$gYdnuKnw@d7=_cqP1(nr*8Sq73;07prIkd+Ux(4%KSa$54+r zZ#V7`>0{f(B2(_y52{em-WhdM8n~n4Q>4lyEG+&q5nYY`7klJ~f-B&;AzYWi zCQO$PV9aedlo39KCe82SZ~OZDPzjTZkq&g_Wbz)b$GLN4QJ%VM68`<=b;k*Cgc&0; zP1OrqvXr@SnZ4&z;Edtras@Tw-_*@%$!e)C5Mh6T9}0fr^6$G;HGFAY8RCc&HKKjm zgTC86jpd)aks@C_-#BCuv$o4L^%iTo+#9Iz;mDh?!p=@U;L;>@|E4pNK$TbK4#r?; zy^G+U>M8hBB7q9=?^j)Ckvo6b?Y|fQo}z{BMu&gd~Y^zRyd_2ATEw}V{ zWTF`Etfi@og)OH=7%Oc$OjrPiXd|2s>W5tWBB>*ML2hY>X?fkH_zE2*18%`()KMgl zM#Jp7#Kq|wqz2$69gZa?jRuljdo8h#BY0`Ri5!mo{sBy|Z!TjqU|N*EX8du@mCU`7 zvi2d*8hGgwtWb-b>g$l;;pUjk%h-#?aHf+S>Mr|{c*IrI>W=q`QhVRFW55>yds--sHM`~ zaH;MpFt@RiY#76eis@N%(ddjv-LUopZ)H)nGPw|>EmVN`F5Zs10x zfY+)|Z(nTvTWhgJ_r(p=(P{JPi9VF7G+G_m%D)};&hgE`_gUZS8mpnTxl^`>qP8Vg zm+%T7|N7!O9x&d&_f?sgIx2pk<9#D9KgrIyh8;sg+Q8 z={;bcqRf;+HG>Ur&;A1g%&Y~p_-+7GO^wTdMH7Z0-5xWt9iJ_0gX$PjimYimrrfyB zUw7t?2{YBNBt$b{11cJiQXQ=do$>)y0-jisAV4P;>K>Bfyg_szxn_&=NmzSEVot9g zW6!CAU{@L{8P3I^QMncCcd+ObM7xs7grpclpVZ35stOuH)(pvB5O!1jYWX~4vcGLg zgNk1k_nnvsO;)>75Uz4q8%rMhC2jAe*DfWe*k*>oz#KaB2EJW#3dY*2bY(?NLt6fk zDY5lfbRt@CZJ(12gw3%vVB-q5{8)ierf$^Fz7RyS+?@giQ$u9GrKLu2HipXRSS7jR z^cNq7jd`bn3L1(76E45iOr-HDMTHFPEUcqXGRJvAOR_n)FE$)_?G=b*RfTf7T$EsU zi7Ce3pT#q0n^>d-5sYNa&Jk79cjOkmWBq^B8x~g!eP0CY1otKjoIY236=x|546Q3* zG=)vBFsW%n%H$$mup(bpm%80nyH;5um5A89CBZt82!-%*n7+AXS|eMzI`N$;R$t^d~-&oQLahdaam(c44CMtmtX( zsmE9eVTS7{H zIK-kiXPz{Do@q`sgNDc#qwUp$8p7LGGUaO-fv1Kh?>q(;Pb5JE9Xk=k*W6%O2Z zr&OLc$GxSYAhm3JM~zMEk4^700~ww*4R-5$*zDg+^C%0i;u@E1%Py zs0x+lS@>HhR89L5sOW#cb@IiCv?L2OYuCVzYc6^cgmGD#ZyO!Pw`F$;#IZ#E!h-`>R1t+m3w4AaQtC&|@0b;{Y(y0)+74&F2Ei zE%t4apF2&s;GK$mz*D3|1OKdAY=HNHqS%wnz4J!?B2a;xxKiuOlB{%bsf5m&h9#!W zVRZW3S;ILfg>YoLP=AG8FZkf5r&*Fq{^(*U<6Og=*s=G(4H>CY_nAK?K3xULu;?p! z;gfx*&3`_+_tKr`v^aN?2eE9}=|E7wx{JXBpIsVZ5vtyC1*~h-zJ>N8@K5Kx&wl(w z&qu+%RTO}a#2cTx6@MrYI}_q$^wtsY4av}4)$_Yj!_%@VroW{_`8@|K?-OPn+0_h#(-M|0u5De}UZp$;bX%YbRyrL`QCKMWo!YP+c3($wXQ)4;QB+y$Tph_&p_@b3JB%$e46wyCh8vW@#EtiV4Oa zwTcC%YNt`5uyby_FE*Z5=lf5~YW4Dstk%s?+4-|i9siH4a|#wLYO?gTZQHhO+qP{R zb#2?WZQHh0*Va`39WgOI{dAu8dE1dY*IN0dZQqhU-lqC#`Vky z$Iu7^Kq35F+g_8h2<=4hMR)FZV6%EYB`cCUEr(6+BAMiaRBFjWmaP~u*vuKRBj4U} zmZ*h7GRd4|?c5RXPiFex9NxFkh-MzR?=i0kYPB(K>^BsP6Alc4)Nptvjs_2tqsXP1sZ_Gl%gsI~mAqq78iGF3C)gT8 z{?|9r*7?GK^pidzPUq+lXE*(sJjItsy9WB&=I0dZ? zOPfw?0OnAP>EdeM-JWMF77ep|Kdc65kxi}8C(03k1gzsJlO%1(G!upuDzG!1Zn)p@ zO!TS<6)GVN0uf*2FiOy}2I?a}Lv^4e!@e-ya=^qq;#*&tj01O#V1Drdq1C2U5I2@# z@%gP9B6Bd4i;%{?V>F>T+6c<}d|_CHR0N+2(iQuI7~azTUt4&x^a>fEDKZJ23Ehe> zflvsW5T^8?nDkJDF$xtUf~Ek#7-P~=xdN>crjP_RWb73SK-0*4BxT^{z^!`xgMU%2 zBawKpCS{Y!sPw?}2}&5w$ob{KY0R?GAh9efNH(G1cPa-+DWb2>k3-t>0dSb=tyA-0 z1Ievo4>Rwl>!TQu_WM&Yc6L2@yKd~d)EZhCWi&PQITkbs{5T9Xgz2^MvW_S6OHaD0 zh{~8REFY&vs+A%=+k}~-)eFoZ)52;Cid8~yt07YHDkM6AUDYZ~2;Gj%;6c?ZPATn{ zuj4>{9$-0}d)EnzwkFoGqmaO6z3bQ zZkKn&MnaInu`nEU6+;{KK6#M_v?b`~R`2+7>DU@?Ifsx2$WuB*<_nB)L6qtBbCR&+%6Ax-;me>a< zKxFRM)#;EXBxJFQ@;O4~o0d3*t1HZl3d;lt*^_l* zXc~NOsHHkZMC<~_lzHGmn82eumKrndB!pp7!UQO)e>PT8Ha2=^x_07a3mSRTglDYg z?Y32q8ClQ!LGI;b4&Y0S7&7!bxf<$CJ7nXnVit25vMQcY;qrlN#HzFtMXa1^pm5hA zH}p5hRW2igEkN{`Gwrilg5_R*l+QZ^xJgws;XKsTTb-E(t&<&YP;O#1U7_DyF1Ue) zjzkP#W%@xxWi3~6GT1SdmF2*}1y2mnCa!*rzj4-_n|_d$6RFf_EnAlnk4sb<6z zLP1oCa2Pb(j`ccTi!+xi$B5Ec)yj$z955o>43XMRfaMUZ;CeVUSdTH#YUL3~&G3`S zq*$kC2^|x;9a5bl|2VvA8^V$y!V|rQ3m6y;mV%MM&sT)RG>9=N2BbwMOD!?WArk6{ zRSr!hLxb-^ghNJYF-?t0gd3uXFZKO?MJd@O0Iv*luxf#(?wmAa3Cf{E{_yKg@9+8l zjKD^BxN>e$@(7V-s|g`U#X4l;r>;KmB#M%@af6XH9{?@8gwaK7v}{m4Im-~~7Q5{$ z7#(C-8y32^6N4a>pCUC%i-T#wLZB{6`l26Z&_czLsFU9_sazyV7g9chSm2e_iN?ET zlU7w(HBK5?(7vTW$_OGVX-))3^q$=3{8K{DOvrUOn0 z!|>|^Q*uR)>kgub@DusfD_1pDR5V_`>ysL>KHM^c7D7c{EEn{KmlzG~6ju&TYFiWN zai0ElfJ+)V4Y}ZuEr*>TcR8XT^&7dMgplz$*w%zu<0Wg;2a}_#8H12tB&e z&prAcSEnG9rQReAceLBS``xJKhwafn5GZp|_YsIW)~V2i8kPQ7Ri>Xlc+5+OBeGyq zTbjG`lI*c3{-s#Z5m;JRy`^ok8R>XbC_}^1Q$T|#qf<8>)hSFjMe~`qPlsZ3m0<{B z2}Dpq&-bqe7#yLvZOb{b0kyC^#b|d z6fS6D?o&i&kec|-;J+i5(V9%3(?FlUXq2Q*&@X0_wB_T`;Uu+PeNCN_d~Mhol&imW zhZ8<^hl9_UgG+d~y>8C*o;h`JsPAJS_r2*Q4MLt7YoApYR~_t#i^T$;lb4E;HDB5{ z?*o}sWj+R4!)OYA8Xd5q?yCUFkCZaszN91tvC)@2*>cmH^p z85xOdGbeAmot>St*U#PU<@5FSa$V-d4{%vv+{HVtBVb zLtPu4O;o1MN@wy+V=V`n701d54Im>JZ{9DzasWdl&c>`t7qDi#blpKGB(K78YPAYs z6M}^iYK~qSF3ui4HF*D5M5I8NG6fah%?Oe^fz6tPa&OKE+Hi_onlh?`3AO_q$fIiK z#HesZL*!0kzEn%N?eva1A^#{C!xLE}FKz9>sv0~9)PPg%*60mihjp2*&*;zh4-L_U zZ6kd$uN8EVP0hIb$uG1+{42DV@aY{@LmO~4P(?bFQvn%Q5Ig6UDt3jaG4Ti7ucfLE z>KV`}WvzSW=TT-XNDwTTQ_Wc#6BQ?pVH)l*7^3o5%+#xP$Mz^wcQ5BEHq-5FEx|5%VC7;;@sOWcV!ew+>yI%0S*ig54aTaHqd(5&; z)m=L2k2X5UXRC&hes(N>fdpii`Go@y-GqS@wHsn>NNqUzyyI`y^q2ou+(2B}Og%uvDG+yhk8k3tSLi3VRX)vGa{92ObRB&tL%LmvgW-nl zd=DM;@9pu!e9LuhJZL)C-Gt}72(~rwb8z@PB@DEj4D6unQhma>vBlO4v8QQp$6(>$ z@qlKN^E4wK{HRqWT+s+&tu8vkMT)2F-elPmh`(&if$X2#Ovq!{9mDgRM50+KDH<1u zB*lHk&gw*zX7Ux&Tbyd5({97_^AxzKFSWLvN9*x5)tdYqI>&tVQ<9N5Jt=;~Zf|xkQuUfK>dBbI zTJv;|^>OR+%r2h-x!)yH}m{klHMqMa<5Bx>7PuH)6Zb?Ngj2XDs#U zpqMBwnkep7NiA&b&=*(jHLTbLqp?xxmAU+&dUN#-|bUn*_zK7)q;u3KDCTKXYcfD+OfxZ$&AtK(zR#F{9Unlp#sYYmIbk9 zV(o8Ol;i_kpZ$IH^S(0tH2nMp{GToJW42?nCISEe5c~g%lsK6>yBNAU8QT4eJ3E<} z{$u1>7`hnQyZraSy}5%6EsOns*m?ijXIe|Uf2cfWPL}_Ul+1A3I&ZOM=H=)IRic6nM}v#mAoU?O->6chlY0^C?A&u#Cu_qq2Sg8@h?Hdc?zwzV}e zyMpj}?A~_+e%=o({ApTX#L`9Qm@YJ6@pE87GJ!O^IYv=`z_KzZ2m8m=KrTqA#$5hG z_h+_5Y)yxLzO8P~kwlTu`srf@^H6B0n=DiM;7$_E$|af(4+q!R$H8TJAsft70y)!| ze@X~PG&F9BC+CoX)KDa1uA6`vB6gJ~CsJrtItq*TH{V*QrCJ-Kr;=vcKT?GUv7$ef zjxHj73G~2da4oCtRZYop>XxyyKkzI@@!B)(hFvL%)S^5FCiS-{Aq;+V-s|I@_@hlf z86`;{yIlSJbd-<>YhrT0N!xCdR203QhX%5DhgvA}a#}GikSa=YhOD<&o}J@YB!(A(2CNh)9Bg z@(@K_^7IJCCrI+}#1euJbg&g}unKB;g3_y%0IMe+M(TH7S%GebtSj&K!H@rWlp^BA z^LG*9+8-}wx2I}GUYDK0E)C|7VbQTvT;~Y25}TW=o+XZC_i>Q6D(=oT2P-h{A{GY3 zj3np2kQSmG72p2wg)?j{=T(J!<<)C}Ij_#t$c|YSNEeJa0Muw9&Y9?-LoyiREK;gb z912TxnP_hPg&PQUD@=>8QWUP(Juq^@%chBVcYzYaZe6PuMt?_2(SjrLS zoDQ*XCQ_(b0gTyNgQeq|Nr?$e%Xd6VUFXUiCvI%)-c(NXVG;cz;Rdz^urYMn-Pk#~ z3(5#gV!RQOTrv)*9*ieYE%k12i{>YjU5 zM@ISr<>-XoPq(Jh_JS_gu=to-EM84)`$e&J*HwltYd(D#mA%@3_aQCsZpol6&kO;L z^4?j7Qrb~P1jd|i#xd74S!{{uA=qe2zAwc|4~R<}z@lZ(^bE}f*y+66@;F8j<=<#N z)s$JsL8)%}dCrr{kdtyA*#(R$`$vH++*ZU!ja@+Q)}J3GyP!^vKEScG(g#g7-u(DA zFr%pwU8}vLW@`~^Y4=N(GLdO;d<=C}Nj2@WD+`sOI@r1a0vd^m{ngmCntL}zV}0KZP~47$`dZYYLNB3s@pcpy-e0z9JpxZCQzSnitORGLh8fEfvEn zX!2yB8TNY)6d7D;=5pe+^EoY2eJm1q#g+MUw#24`##Lo|`s|J`%i8ABLg@Xw0ajAq zMI$U)7P=KjrigG^&ToIO zv)UeHSoHh+JoSDSyGL&G0FFMUx+vXS{Y^gym1fzgxyUS=KnmnH41?HRJ+=S(r)uc4 zR(e&U4R5}wS?y@L!X@ogC5h1oeX8ekJ8lszmicKJ@XHzFfbJU~e)7pZ zaKs*k2*^Fw(ELtY_}V*_S^qIqf|pH0kbfn0U2{SAZ+_%$QYyq=!#UBWEBlQj@{;vuMIgoaPN8Z z9K&z>JxdG}y;x-S#C-6ZmF{@~7n*!D26u1cU(%Y;?8<}nycCT#wyO-gE9;oXKoDtU zJ#G<4904J_Of+aIAXF0#O%%hH9p68#QK+sqU#wK*L*%odGVx0JzGdR8z6vk{EHC?P zhtYjKnQogGh`mTkW3QE%X7FpxmY{8FZoDBDtQuWnwtg5MCp6cF4Y z%lNQ@U2LH2LyOx3Vr$raCWDRD4#V?i5=f?tpG$11S-Vil0>Q2O>m=>`vq_zP;VOyU zd~Pp2_*{MyNOeQT;&dLCG67cchSS{@!+-A-#bj8_mrJuCgQ1~b7T^>c{SUzw05;S> z2s&EZ$OH++y#g_v#Rfw?z64zzfMbk~ih=}-DQCyobdo}5Jcop1LOP{fwv6N75*T@C ztkNYSxzdH!PuS~L-ka{Y1D{)iE>LZ^s8aB)n;2WU-0X~flNK|S>gk<7B2%Ty-BgCU zTiI|bc31h8VbpZkD`F<5v6(APp+zx^P7yM;VGl}+uj62XEn8`K+xo6R#{9 zSmY#6Jpky8ZyZLR(UJ+`>v2UKkwpAbJim6qpm~P%9K7oW3#|ii{1)Z}7(hbkawj5$ zL00*VW@6?y9Mi2>qXx|PX?;AmfgM66$*2q@vWrY-Rh6XH6a+y|N$K%4@q0#SFda6q z)A1E2Ed>UOU7svFG4^Dyy;1~ziXa{82J~@CIDn82m*BmUT{MtiG=W4Ljf2KKy(thi!cwC2%_4X9$f&2_UsEt$wbZ!asXdY>#0zG zyA2q#UM>3BvHAr!EB3J-LX&Uhb=jtb^2A66^c03UppIb7ON*o-prL^z|GvW9D(S8q zla^uUJR6qG;C>h*eBgxwgkP|XAl^gCNTePyz#bwbrk~hwcjRUlCUtPCf?Gww1lWsO zCKBsmb zba3u!5y|elC!kxUnXO>x3eH5Oz;@F;GR=piSw)vuTjygTHV8=vzwnkfl*pD)c=YJl zHy-yUm{Qiw6ap2qs9dho_-g}O(4D%SMR?i&SW3AzZOrO`y%(L?*|AYJC&GGxB)ywN zn-$&W)126t{7&rb*_W6=)CxE-=EdN}#n4evro#%SdUf4EKjn0bB}2GuQcWDLCMK+? zpVvg8$WB~WR`5m-R$J)LRKsq&jJwq3Q1C+)3CMyl50A)XD?q7!_V%Av6o$ZWt5E*^ zs}+-Abq<8M;&e*vUBx2?=!8CH-0Y}~&Ar%^04h9yp{hU``vpgL4d8QJThtMMMG9Ly zM6Sq!{Y=N0R&3*2*fqd7Nniclp052O*R?3^ie`~YuEx{6-ozQAPH!9spL7~i=)*co9n#Q9g~ty)|m zMF1sT#BU@c0&nOWJ6+bM0r5Nl@UV(wTVbHH>0x{CpJSSZ8t( zVBN$X4a;5y5P*8{YM@UsN`u!l-&8}Y+d#pO- z44P3etu|ZM4TKHyO zU})ULx4~bGSjXbYe9#gTlnselIde0)4N-1Hh6lZ};?p4bIz+EvfCd^iQ@Tr@*Dhz6 z%cnz2-hPbjnPAPX)<#ywN<@c6HFKE^lK{acNN&=)QK=_KH)VSx#yvcqI?D!IuwXR= zx+k+;%lEBMp8t8=O|O7itPlghn}pdpEcq~*BtThAA{Eq9z&=H&ZTI|3QQl>m$J(Cz zsf=gdb6yuNbH8FA`)b>)$rs0c-FC9C9~&>u%P+G5qpUP5rc^TQ5da7Wgb$LC08zFL z9C6NuMh5f}3lCcXuB8@|%AS3zGA1kKqO5xB*(6-A;)d=1Ios!U;FbS8PhRQ0t;yV* z{uRu&!1G|kJruilKwu$Ledqh^eQ;Hxu^G{sx6Z0;+yZWJ8YystJ03K@)>S~vnCuII zSwCd6vXj{Ih}7_J1_I^6;4-_dsW29;E|g;6yAt&aV~vEN~i{QZN_l z9tK+2A2=Uy6cfOyBp~^yphJorpjqL({p?d4C{dvYo2|89xnwa$C!NYx3lBhP%{n;O zx02AGPD-fqJ*NS;&Ys)Rf!A~}dAHf4U=mo3Y@4j)|uYN7lht4rP?FM-3Ess`on^)~Y{zknDe&Sap zD0=Vt!&AEJ0sG-0e|uPEB}7fJ&hH2P{mt65O@2KzAH$yIUuA8zXX2M%-FkK+r#-ms zTqD^D|4y`*BMi6M5qkEd`RL0Vh*fUAQR5Rfm>SIWSK$S&i>@;-9M=u$Z^P`ZXZP=MF zCTC^)8}-7lZumCO8~^_lFMxyT>@2VW0LIn+mw4gqVsC8y{~-OH|AGBo{!7aE4=vEe z)W+1@$$HL(?LYT}^W=^;m83Ry{L?fvpFp z$sDvEq=CPozpp>q$nCoPqEyXSwN?NwPJ=!kpU2(5AH!d!q!FPuHAoOQXvr3NFrF%Q zN}MU?(w-Tm#wD$|1S0dSBfqFST)&55qvv+*q~`vHOD!v>l>b?cgsNTL2$N=Y(n9CA zc1}S5pz>*2QR6DU`SZ;rJ1UG8H`W)XzI}&;gF%ueu|wYjwx;zyuunDZMU>e950=N5 zVdOBJU15NMm!dWQ8ruBa1`n3jUH(=5n~{4ThyP3Md+&Y!x^uCYg@`%FM!*+P1NF)l zU$2SX`+017z-ODk9)vmnMjuE4$j0o1-OBDU8x_@%L`I-r#Y#1x)fEDR_=A@2|e1NLsiQ(5tSb`kbf@CV8ds|y&u-xq$FXk zC;&LnIKh&CD~%O&DaL`f>ygOZcDdLxlx;z*95C>Z4kHT$K(4R;z(+- zZe0_iwg%S2=lgCl%y&5nqErHE)~2ILMsNi5hi2K?fo97Tbe%9*hFW(rdLw2!2bamb zT_CYo=_3)jF08pxWmQuB09mgdx}**Y!TL^|Hk`V3Wi_Zp1Ze9{T40o-9t}iR;Q9j? zWXh#P2XM8qnJQ8s0k;vQedCv&dE1VF#*RQ3O#16rOOGg!8wFl!1hY0KJ~9gzxn5M0 zWve?;?fl>5jzo5tZ48ZL_5vsa6F95pfIh@uj+%DX4<#sr47xTjw*2bu5W=3gkc71S z^kADJlU0kL=IQ`x1|QsWVL^^xQ*GdEGa#r4s3GP<^T=fi>NRY0-Z~6S(2KhzX$B#) zSj&-GwtY4Ohsfql*o@FZ3?|!kF?6G0zuj@5?t-LL%AEx@M@nW4ug>^9Q%ETfNan5u z)j*qB=<#fP->&g>>HQTDCg(Xx0|S=CU<OG@VB1Cu-*o7 z7D5c^q#O$K=JKz9?$nbA3syC^(VPJ->Qd$ULf~XcL#$_%Wdt$J4kM;nd8lQaCXy7hxBm4G$0~w;Vi*|(0o~kfe7ZGf|J6JPKG5q6#z}4!2 z&{CJFoC765Q=r~$prJ4vq+>DL_+P5l10Z|f$lgswz6Dr`-#muhCf&JrwQS9<7PCRR zg~nFm*D~zlDDRSc#K98gUD!`$o^$wDC5$Gddkx5~v;o?8NCmpY2mJNOsX~MYNb+RT z)SB}T*_!6fTJGj73U9&93EJ{d1maCXg-m%N`_3+W0${n8x*Bdi6Q+USzdE?k^@J2$ zM*M{xA~{;YKts56RQ-;cEQ~jscQ>PXgLufvPT7K0nY=-q8M*-t?Jtk5tj6XuYh%xA zb@MHS+Fa`V_5{P){>$8BE6mZP;n#1^b7T1O87i8A2h6y{x+ou&!Va>iIWrS~c+aFz z3=#^zGs{#X#NN+!av+v(eZ;+k=eB}e)Nj&LtVx|S48L+)PS=fSOp;CG44d&_CkiCG z4D(l26Qo=4m0?zgU&M+!0T`xH8z&)Z&M4s;6u|l=EaT(sl;rCw{dqqd_Ez^tE zG=+H>aVR!LcjcJxMraK*i zF|T7TTyB4?9KO}&YBUDwImdzxF{Ny$258A^k4@wG?H)UaD(Rx$+C;VMZ5%>#WQ83< zn07%g6W-H1&l+LDZpdRe59bqfb**!zqJV0)sJ=w_n^-!)sUKFb^Tug1Cd%CskSSq{GMP2 z`StO;0EKOZa1}|hcHwID8CIwWFZ`t!#iyCXNb&1sWO0380DZ&pfSxdE3NKjfOu(&= z@oRpfq6YRo7nFrnJUlGT!?mXf{%kagzcgQ(8&=`g3F?Iy1qA=Lq)hrfy2Vj;lOxe! zO|@)=(4r)kHOAYyOU%0>dkpP<-yQ5Cta1=M>fs5CoB_f z^Si)r`{EWpoAYyNjQ5ZCB_5N=UA27r7Qb$H>O4kqK<6&bu&|#KWR0$ZrN~DidNaK} zVcfUE>0@K5rk9F$)8`>9E3LzV+I6;m9dI;v>!McHkGmkHdJB3SxWE5gI|Ke&+qk$g z{;gv#tDBt#-vuqw`xNlA?)Q|dKe}mM_Z-QJ`@JE6Dz;g*jGp`!7ZHR=WT+UX495Li zyh~X3kn4}n+7^^c{35hzJK$bpReFv%~@oD6(K9MIUTS! z2wj868F^tn6>;^!X&JnVnZfB~L7%@oSrxhmU^=WgfdMW>E=~$`?rVSICvNUi4lM#=hk>E-AL$zCe z7UkPPO`r&?{zW^cS%&uE%$ z3hKaYgK-PD%40O=_r>vKSME6^r{C)*i@CA4+vB&ePz_gvixT?misoU(p1!S}YJ@w| z;)6vG6>^Pz5v3Wf{kyj|Y9|k2_ z5P!dW4g(UV8YjQk;~!Ljoci_hSVR$*xrys?X;+t@EL`2WLfTg9b~?0dmr@4tQ?#Ad z?^LX8@F%dRIM&of>m7NA4y-6buyX4e6G`(YCI5>0el>og5rnWK*l4V^8?sWHnQKRH zm0F7Xj&WIE__BMC9By?|Ew4{*#|}T7J#c0XI5W1*BAn&+bMolZ3BGEj^QEGAWvF$| zW&~&mZA_@MGzQnLyW(|uciGbw_B&kx_g|%dJ?RH#^?NxQE3@FtOsnnYz0Qatn)XwR^iko!mZ7zbQ}8&%^QID_Vz4^yru;RsreJq7iF} zN~vLkBl1im?=_-hN&7`hI|>+G`GsQy&V^grfAPF4?w?NSI9w00QmM?+C1Qw*8tdcp zaQfW^Z$4%}m#b5@-M<=8(p|Le3V-=b zEK8UNS8}p>?_?GKh6Fak6quT?&i{7V6IZ)y6&^9SmB!!m6`(y10OE+LdT1R(oKq-l zwUwr}cTR9}wYi2jcKBJSkw*`eT67@{=meY{vTXvyKK4y(YnQb~#(<6YHaWC=peo;f zAbFl5jsALIT+YXR{&4m@2C{V`4#{pkJxWDnb0A?eMyAKW4>#H1$wSk#<&6*)eTwZKZkn<9NX#Og$ z*g|$VRNlq&;DCtM0c6ikFv9pOTBfT0rb|GAtVj<*GP_o-|D8iiPVJE*45d(I*s(eN zBqLakVGV07RRslE?-C6}vJ~1DOq`~ckWZ<7X8#ZAlA}i> zAWYW3E`67$uwEn~|K~kVGsc}$y{+5mjIx~Y+xr2jeJ&Tb)tZ~NWJ)tr%&}2Jq{<~jy#6rBCzEFM-pcjHhC4dC XB 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

aaha5$I&yWep?Asr9bj28fADE@vEHlmx-X;A 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%0A24lH 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

;v!0#_^SCs%)yTYx#0hvy{c6 z+ZS81=G8M%6is5|w^>C%;eslWM+BbnC`v3$25bmk4xfn}3fgqug%m|!B3?s<%!x>5 zQI(APhj`sf;y0cpckQl5UZ900!}8T8wRqseb4xBYg&8`QCL%2eR?-yQZRHD+w~Nm9 z6_ypdS--Ec$!@NCF=h2jDT~TV7`VdX8jW1tqm}@GBU__d$aFijL&p=Vq$f*@>y^4e z&+FZft^E0C@(=jbm(-ic+qa!l_x(ovc=Zz@J)hDl|IxBe(+ZxhdfP=%M~&S~Q|Rcd zi<)W+VGqpJ1K(w7H}H_nmZQG2_|>$T{@v#53~?76tk6VS@?B?Cc$dYkG8SyO#zsDN zSJt!Vyvu?*cA5>>(T;Z)8o7ld-GOYoYhh5|P%Nlug(#t5U-#gNHzKUyG#=+%9=waK zE4?dj<<;yWeE?-T^IGV@^=#|&&p!L?v$ycSZ-?-|Te&Fn`EmL+i<5e0{Jp~$kgfaj z=f#pBb!^7xYQUedRm`{c$d)i2-#4O&M$CuWP<}|6SS!ax(^IO^s$VSvU_+?^k@AF^p7y=^+Aw}VFXE==l*lMMcFzP9sQxkfFa}vEAg#q?~LyS;8%ejxth=;p;JfqB5K*8 zM?KT^X=J&UA)+Yo%|*~(VjXlSS`3Bfe*NYDJO^#8E|mG}FaNZFhnw!@D6mEamSCwv=nSu^MbA4871XT^g_`pcZ#5 z>WAE-9uo%hBRgVpHaYXS%{)d!$I_u>R79B@1n{D{=KI34eIr`H=1H6a z4GHpR5kmF#Gbn$)DQ>T@4|b`?SuGu$TX76VMVJc|%UE0q$_ffww@NsupWyRz1xD(n zuWBU&LwD`Vk zO_1E*30VYI5HdZ`siy@YwTy_-z%nhWX@UjZHyjo%a;%Q6zBN{CN)WBfL=Lf<8nk;A z30X7SyIuL3zV9m@gdaJ+DQs%Frb#W|b18F~N8QK?T+c8A!&p?3mu}Ygd9l_#&?H^s zU(mf;1i~>47t;N8gIZwEWx@-otvkZ=g{epWDtg`=ui1KRLT)iJZslPKs(0_qlroD# zsdH**hD|k#>duX<>DZ#rj-DTNoA+em3jR5ju8N|wN?FSUz?46HrapirDodmag>H^| zEwCDyD4`@tTA)J%OSnUz0g2NxBUdbmIX;U6=|%~JWBlpt_&LN%Oj$W1Q>frxRO&$( z+zWJ^gjo&`tR-`Nf}@(rXUUGAKR-M;KYa8%u%cntPBnWR1>YSyBFL||;W4CjV=3dg z;3G0o#ot+4$4u+mVo;Zv-`r&xziu@0YN9X`c6e2R7W6zlLQ z)?u?)hb?tcd^=paQg&{REBHs#QU3S)&FpO-*EZp;^chBBV7QD?1H4yO=rihtfkC-# zMTV&xre&E6wuu8U1>ERd3fmS7&L!toYs3%5fig9~Th}E@_kw&y{sC|3BbFrFJ6~R; z@}rP$YQg_hWGS@5)}tw#ZEwRrcgRxccI&1%GY(e*c!GY?=18?Xx3QwEs{7C+f9dI zJ-qB$=JPtuLk=>7Ut7zN{ zTxPkxNp(w)sO9^d2DZT|GkrG@y5(|xG5+DD2*^Rst`m`yr`b3TZzgzn-|uq%JS$g{ zz3EHmxS`!d_+ge{Jb|MuUe_@%CtG{O^)&U%M8FfWplw+7PaUrGKmPOo{@?!lfBR2l zFz7CR`>YD+Oy>2F2l~!n0Cu8*Qa=PI(q@v(M+1C*c`!JH7iwkM9P;b`{J#(t3Njdw zUw`>u3A`s-5JtX^a}mNEjc#6MqzH3169h}6q+U9Tvk^YIJQ!fI(YC3PVh0E9#2u)C zXIYXIonfoAj1xH`#Mi_HiQ+UaCW4Rf;pM^LC?dN=4y?n#^$KH5A;MrZlEB`L5afXj z&Da|Z$SJ!fQ_@;$}D>HcGat|pNWu=}g$S?4M2f}}!l!KEFJ z!HcOR$}?$7Y++C@m6Pp4z?0yx>m|ImT#B91tMx>@(oWApM>m)+DAz5puNW3(K6-Us zg9}4Xe#{se} zE%lPB`_4%!3@ErF|ze4{saxDU?(lglWmUBAG8=OP;XN?62!{%KV7a&~^jQGpxXN!=-yb8pUJqU^No& zy$+my7cI$JIC2oJw~3bY>;LsnQc3DbX*gidVWE3uFnDr)enM2NBkt_4zx=aOf`0ww z{}~L(@5x~B>o5P@(P8j+JfSLon~}j|`E+vUibl5OtH}hOP2L&EW2k-tr0$W|@FmOR zpOAUJMnc9181~5Phl1rIC*K~wIDK_~{LSI>S5FR44_`~_JQIlsOC{Qx`QnrWSves+ zh;T#hN*+e2*`94^)VEE8TDs;@X6x|oVBp!VVQV4R){@6Ul+F84$7brs2E?J?NE~aJ zQ-?@?{pFu;2u14A$@&%-De$;`&VgQvllLXghtSr$u4j&HEDsS^KpmeF2d8m@4t=J*k{IQV$%P*5il z+z~$0b(gQD3B{b3V>aJ~D$EyY^!>LzY61RGNV14a zytFF*0M~)K=tfnDLhbzP<$#{oUlPWDsK6o@$(^1hEG0Y!m%*Lo2^Rrpb`(*=)O?U9 zj505Bs4JX^Yq}osa53tv-R{-F-tVx<@5ndqy2%~y?!peMFYUoUi^nO3`;p*c? z75y`tOT=G_xjgAga?}d<`W;suQ$WvMcPcWt;YN&6&50lfA&98w84)!@VcLv4k>@Vv zAS_G%#hkEr*emPC(93(hpmCMt zzXz&t-tDrm>N5UG^8YrHpLv?Wh3`_s^g?PykVK^!7U%$G6zEpuvPHwDx{cwr{T3P$ zE3=&Z&7xv4_k>;F5Y`Vq6_xS_j=_tRG`_)}*P?b2u0)GDvR2AS$K{&uYE+ACk6LP+(NOnvYQdYD3=-->i+hG?*kM?+lb#n;no$X7B?EXmEoy*e2uSBc6R_M|Nk3of z@Kui3Na$W*L{zgOuF5w!4Fbococp@wIzcELZ^hcc&L+>&ot6bho~R^*+g26?26t`7 zsc%~rwSY1LpX-$K&=5A$4Xak6dMj&=^PLv9#0e6Xk8j7G&RoY7PCyNVN7QoRY+E|! z;+lr3bH{YpiZ!-TSEQ5fwb@E*<*;6w{O__uexoC3`bMNXT14$ALaUfhnPYOwcxYH! z;6{eCcmzAfL9{A@h440SxqQtc>oo11NOyB?C0ICF!LHPClo9L;k^LeKGo*(-vQ?E) zL(TY>UmdZ(7@x1+!a&lKq^ug_;-o9t{K6g;;8h z2A*M2E(~TETnHmtVUO&ZW#1F&D*gWE^smhd4y?&?jUoeN9MwHnrIe^zeRohGq&Y z6H20qM_;7%y5NgIbva7TZiSrgrU-qlXLN=Z2XrS=QWErW!Eff z^L2gNGrgJ9Y+hsI^Uq+IZWyE*1*$Bl@i6GCAs|IYH5Fm#*w%`q_AV}Pfacd&rB`a7 zcXm=jvv%~AeB-WDe6Icn(XpjMnC<}LvP%1YD^Lk!cUnUm*!6yM*L4@}H2G2`iuYSX zUBKi{Yv>h>c)$5;HKd+jg<0OZgxV*&yoP#&J1#>?jCY~M`#?hj&Yc#}&3=5p`8%BB zx1`4IaCchAQFM=oK{A&+vV?Wo?E5mvKF__=qHe7L@V-a4e~G@++SZ@nci6V2JnIfO zY435jEonnU^~QX^bsF!s&^ztxyDxVoO)qZpci*=iRu(zyKv+jS-8+EIz;_n?%e4?X z>+K~Vyq`qs4|S(yu13ReEH7nIH#FYJpm36lP{db)|8{KP_S-=hXLsqU$z8zPPd}Ao zqnl@Kb5Ygs?(0HZ4_j0r#1bmY&*u$J;D5efp`qLW6qi_7zVX)q)v*5W&MxB_Fj(bZ z`F^|iY^ID2-+gc`k}#VJT4oe(v+!*Pg5bWx7<7z#g#g{Q_c$&mReYyh@H>^mnoWZ^6$PZXhHS=yIEl-+&S4Fz0zP7_AjX)sV_*>C5e#r8@`6A(@w~(! zLyT`PL|Ng+R>&N|`^d$WNU|A#I3h>o7ZBSBivz#VEYHR{PLOhRLVX7Wk;efZvtl)> zA*q&z*>qZ^aXH^@d!*xCSsg>|#7Mmw@)D!)A^!j~b#C57XbW2?+pgI^N(O^}U&Y}i zwkl-&?VqaY41lU5oxdo^k4+mgu43s~yGXw~&*$KE&#EZfmo%%&-DbLjNtPAT z8<&Hp8i^99Wu8@X{(~f|_>iQt=@4A|5uLyjJtO#kob`Bi15!gA9tgLc^626p-D|B#DEZ<#RdX*Kx{~?{79+GjX?fJ2gf+193)Ginbw^#N}L0 z4uBWcY?eV>JQlD5@>^_+Bn1SoaK6v4SQ-i#boD}*YCu^m*MYRNA7qup^^Ii1pc!Cq z9mT+>?OC3sS%s@eDaISnbO)2f3yjH^3=ynhgr##pnj|JjLx+@d`R_T&%8AIOZ-2_t zcvfM!FGP6*_m%BH7$<^cIZ0tuE>8m`2USsK)A%QF&F+y&Jf0+T62Sv#;H#~(BAX3;r zHIED)BG-^@Ulq`G!cv|HRfH$XSy2akU@R6|Lp}pHCR%WvGLeV-8Tj!pq-n*o-_8$qXQgr?Wh}5}gSHsP7X@ zV^lI=kK>}KaK{*l;y1ubV5sjN$K{hMAi0=jn@GBykzCS47G5IivsuR`J{2Y7t)lYT zO0}X(+-={_w#v9nlm$p~hFUQ0k#AwCr=rNJJQRqBm$d-eZv_x+!BLl8{D)Syb|;G1 zRUAt9(W*7Qis9YEVplaA#bMU%U0*uTPn34Q$OJT5sx%6`(BQhDx-dd&h1{baR2B&R zz;%4vbi=Oi@w2U~SX=|oK?fuRihRYzRU8V6e+=U^#&p~wWQq7n@Ae$mkHeuNdI^wF zedLVvAyI!#A*Tj<`y*#${9~LJF&BA3t&wex>P!|n*euW9%wH5j`gmCphdlrh|I(s& z>vk6S46Cw58+F(deo}hbxze6Nzu%pd)8tFI{N#lwzj|^0HTAlq@qUrYebgx1?FEtu zv*|1s6OrO=MM^|SQd*^}0I76k!-zZ;EKkVml#)5{xId^3Sm;3|P55AS%2kKWqLjTi!Y0kH< z;YO|cMJMe{3INsX2KjnB)E5`);l4>C=K}OdOj*svxJp=_%w=XAu;k@Mj>rKd1)(td z?o=d#6*6CQjdGLACKQ|O?!f*gNX1j&o~Sgyfl9|pGdxP84E}qZCUX+Q8-GC1*>wuK zHq@P|X5*Z3$#{D!D@O92zOaxdDXR{f?E%!-o+78B@9jgeirqi&FJsk|5VLJ>bj3iN~7c{WT=(!;xy_z&4vq(9BB%Q0;EO( z{<|F>ijwV-v5?VnskjzWqZkpqyG_{~QXnh9a)7B>ZIaDkfN%32$bFT0Xt|KL09-VH z*6a^vm8oh%n5Tt1F}g_Y5jm1!xgo?x=8Bt5GtQC@{to2F!JxkFJcO0b!1@q}Lh{{) zqTln31)?ZvGc=8|oHj$Ry(y^^A7R#ES|;;7$rjjE7V`p#5mVGM@&nwTrjU6G(dIQ> zL#Q((v<^uwcoo*^Y{?vq=Z_$hE$Dr1CwpqVo6KczJS{R{DUigQbVKoV3%FBnab^{0o?0` zMmU=nLp)Go%HpKCMqLAwRsXK3Vm|`QC?rnifFWXbyRTkkAJL!g3>)4VZYQCYw~DTA zuq5ERSA~y$SI`Vgce(FUPty!)d7e#u*M@pH0dsUS3WaO^%eyPwZ5u!6ZQ_a4`l|&B zX?S9-w)}mo_dQz}m|My?1+_}XaqTVy@{O}>oQPr~L|N?CR%F`5UKYC#WD6>r&=pPY z&zgRp3%~IK@fbUkGwcoQ0DF_R_YI=$p7DEeaXW_B2}Y^t#znjEj>Lzkb&&{^{R-Y< zmJ7yl9%*Us;bBBhK@Wx@zUzIscRcJUwHeLAGG166Ku|#Xn*(EjRZ=9;sPTH>k8qU< zFr$!Tu>&l#iDl;2HY>B{f{!|J*J%NIVA|Bk9uagZVy{@0N4aM6A=paiq>WjuhldK< zJ@JngN;IN&LFC|PD`mZt{Ubu5h66{Vrox6)Bo!dtd3qk6+l|jE-?G>RFU**h6G36e zSxjpK_wG)u*EQt?y7cie4}hdt%4l~1??UqoE?HeL5X8e2bhdIL3emynUgH#LvzR)z zbJ)7UV6R3_me;o>tcD&-dvpJsbZa-=0d<{TyAoTtHL$eW777b#E`~L`!rK~^xny$8 zUNgDyrqMYlNYJ3c20@((=w97Ud;8xQ&>QTkfh4Rj5@Z(2do~Wfjn%Q#a5xAQnqM9V zQL89@ajPCYctDhk>7!Dh3OK&WRrfRlMXQg&1b=H_?f@fi_$-PC;vGI3h=9$E4P7Rf2Gpmic^VB>S}z<7o_+ zW7)$o{k9jljXK9cq@(NjGM)(@vr(3hcj2#H==~~-ULBnszj92!3Hf=2y?1cx-V~

fJj;esvLbpAV*aCz15-TnJD0DsoaR@dX@xH?A8Mn%|Q-X5`v2j z5DVUm{E8)gV&bBRr4g#zHdbwMeG63963+s?fqgrRN|BP+&yQce9+KDR$B*{Eg@2sx zKY#RE5*hyc;oE>(AnTMa8T$a{ z#m>?cKV!4_|8MWhmfJ|O_O3lok=^RBnhX>OfZ%HX3q?w5_GlZDn*PV)L^FX(0M#s1 zqH7_A8Fz&59lmgcZ+!J%I{Xg4^PBhy{zq_}C9|>$1rP*5QcYndf-L}5nJ2fCXZ_BG z4I|wE3*p99d&BKvgL+s)@9$)sNO>$qC^#~*<6{X0B)r~5C@|u$b78MU1AGi8#y*t4 zwJ$hAL{60jXi}N|+ZJDeHUWhy$WqHFKQy(bv#aF5G=uh!0^iK7opsA@|A zcxQ#pC89tpSs7UY;E=#Wz`xiHGl;=3nvL$cjdM*g#AU&+NcM!w8zZxfp@JTCz>i%X z^#m=lT^+0HEd)@j8_(eHy;@dh6EHif8St=gDGS~(UWl57R%Z%$ih9QFIg$`3h*yl+ zpU)FvqwCpz2k+SJi?1Lys_yl2m}dJ91wQg^Z4f`4h5*udh@NymAl)nxLy$@ey~HYS z8XGi0ka<6*n0sP`?e_~Co^PJko6X2)g^2}m3B}6{fS!y|Hn%(2;Ttje)A~AjMbCnv za&+4z9tH{RducFKp3#Tb_7PVONp``ByDEk<#zGE3eMccO7?SM@2!(_e)eU-pISuqK zm(0{lOzTMPB$0$T58^dG*DX-@J`z7sA#`yl!{tt)jyx{1?ty)J8f8Ihl9AA&j-ama zyhGwEh%z}Pc}@AtCK8ywmzdV)A`>lIBtc><(xm8(LNDD<3gdU_JaiNsSSuCV2f$em z@xfM)2eV|w9$)@pt=Q<(PA#6;8}>Z=L9Avg0&`iX*O~_1!y*h;h@2YFvcORt%Ly7%(G+l=`U~wG-4tHfRE(A?8#=ygq$F#v|toun##O zkK~wB3v!zftQStP?+7JJ+_9-`-}Q)=_Y`nZj5$R^gIU>xQdrFW08RZ3OugWA%C}va zW8K51E3>VcTW*4b`38L4Yz%X~<&k*BM@E5(=ubE_4%UvJAf{aZQde>MLY0{JJzV5P z4n8w2EI;uog4n{OC@CczP6*=6wGnS}5n$R4K%xQ@0d^9yhf_mkVG7%Y7lENBf>k>3 z)utkbO*Dk4v!YBF>njZBMoZ0ixiCjPe2l{jaV6sb5djlTy^q<%W6dF)qrsL@R#04T>CEiheEk(C-Qwn3?d0u&(k#=Zw-SD-+044NyLB!JIyq!`&wCge<_8(cZX zX*C2St~3d>7!JTQ3vM%mcW4_$Kkkf)VGlrYkOAD6!LJ#P2w=PN^{K%G7Q291gtH4* zi4CE&Yz;96G-9f*wv1u=*hC>LDx{O@=+XTsH$x;FK+%40IO4 zK!wvecx8-dly7l+X}DvBFc!X3>$E`u|!>KV`H*HoqySm$NwGaPgCnB6^Xj&qs5NKM5g zQ#qNsb)&!$f!qhQVPM`>e)2e{(`Jb}kOM1$8iv4H3H&Nm7X^!*0%1pv=kyrQad3AKXt23Z84`N!ol zQUDr_xag<#n}w3As+gX9<6$?~cU^G6dt%NI`a{{|8YU=hTjk0wJpfKPTH7^ z?Trytr!<`iv_rGU!7})~!sjcypjUL~7u8Lt^z#C-MoT>r0O>9J;+MDzlj-Z7&X3^> z?Ylh@27neF?T>k1eMBknxl_WNncj2UXr*0BHNovp`84_3QqCkFtW+D0xJE?)EEjka z41<&;!TZr*L+HSRPc!vb<_eg&QIObCYAt4O7#Y^;GmA;{7SU=L4dnHAMDw8PJFyL>pclq{_KZPI+%ILC&*J)uKGiWuM&*ZJ+t_#AW&{053io{FETqIU*aVJ_J zN`|_E9g3(K1mzPUu*yyEM>ecUURM%?3?RRMxiKkXd2OT-+%9yX$Kqe!zL|4pOh`h! zn9E zaYFT>{#y`+6!WSg@8c=Ce_w|>Nn||UFireSO;o3n$HjiEM9Z|EX1!kU*_kJi;5TS9 z8;w*hVCM!d<{w~nc+r5wW&VQ;X7%Gspk%PX0WNSt@9Rb;73s!UaGv@Vd6Tg5KE zBeHlnXm7bj^}l%ivnXro;=%}PxCe$%oyyd+HX{zwvQ`0xv5xA&w5rblfDPrP)9Duq za-4f}0hcZnx3lR!Vb~Ed*jb0FKwhfFrBaAeH4IQAz(g4sVQEw>-+YiUnqsUhmDv2_ zd^IWcnGKv1YlPOy4%Zs8%gSa&VllWCL9n&f8J<_pvNG(G)P9F z91C9HRFCq+hj>^o5`L5iJlzxdPDs`4LGW_8&pV^(s7Oh$wV9zZLv$a9D;okYxwNh@ z<#w&>P$TjU#*9vxk5}l*y{D-lKk+QhDjN~Y#wQE+C2&cJBcYH`AlCXbsw1y)EaHvq zs8W^$wI0F`Rec1JJ3DzxCEW^Ff>kSO(OE9HjY`k-5hhmIPbQ@KO+m24xv=5R+L}n% z;Ynn%#n#rVAGh`a{&_?s&bbTGMc?ECunozB{Q~n*{}m; zu+8TWGD)s=?C0MANNn+wjFp6MNF4e#Lne&Foe=8GIPg++ws(;5szBr~;H09|h0M_| zOVer|qo z_AG{1nPSp9KR;hzqEXAVcL%c;H8H1IiyofOtYv9ihGEMz_P~ZMWEEDsyidy(3KG^w zHHT@dK$?@D1OkzDM&wd}VO*)Unz%21!fdTs_R^AcPd6F`9+Ff?azTRKQMw}_qX!B3 zzR18%cOP_pI?&ha>-K!%h13he5Dh$zWZ@*OrDfDoR}?NmcM)WKJ$K{~rT}G8IaG5b z2LEcWM!rPW*I8Av@2A=loemWGDZ&!Pgjxo7HN++eEG_C7l~yK#<(UY?nHoZl1&7_( zEQzHb%hdGa$;kI?KT{6n`uZs*1m)=744LJ^iM*Ag#EM(I@d#dHOv<<*W&Gg7F~@5H z8pdTThvJU_T0o`0(vJhodiZmou7x4axojz`%_o*~X7;t=`5W0aMl z93>tThp-qKu%ezPqp{~$3OqRq7)pA5{Z$4F$?#{0{w)3+_)3e{v}q{280B{F11H9a z4_PM+5sWRJxzLC=+wGRu1JXQcNb_XiK?t2T(QXh8 z0t<^Lz%yAp-pCSX#^{Pw1DPfVqmg(^yoLeY501qfFtJK-HGGo~Dj0?;yIgG1+S}Wn z5rKQVeZ9%yIl+ukR;EgTTs{%e>}`iZN6qEmY6 z+d0u|=7d%SrQX8G&knZ97jp2q))Awf&exR2Bn}Zs>qZvp{4d-a^xEuAU~4mPl1t~ zF}^6oYr+d40s$&Pcn$Y=M)T2^v5#Y9(~^znWlVs24m(}`iQzfWIlvanAwa3pfQL0k z8zdZtQuwE(7ZSz`Qrap9_f9#uH=Lo$L|a^z$IPYORINs%r96MD(e~Qgvghq~y1wV{ z`g@&ryVKilHNKKu+FLB&BP7ha=l7Ngv4BlZ@W~OCGMSS4`V`Y!rzS|pS>&tk<;FAq z2mbar5Q0p>%v9GCdWLquvfxk59Dps*nT4B@UkkK=F2z!xt%So5)C#!w`FkkEVc8E* zN`i z0b=O9?iiE&OEO;y39Cn6K@~2ua)f@3KP^$~oM$Ar(>sm0ZM}-P(|6Y(psh4*@WiF3mVwMgZRiS8) z(lwiFWlHpX!U+2l3f;m6m!kSW6C4l5Am%1unDelnCty6lBlnjQN%=7*Y7wK!=j$8{ zHJr)Cj<|^dpi0!Vu=x|eJE-}^M341#aen@f;3ESHDDc+FLm@kO9ZMNFwq-+30Va`g zrczDy3{IU|1^MtwDUwu@2|<5ul_E(oPA)>p%p3;s${;EIqC4XF=sxy1TpDPa`9*iU zVCp_@+6X$F8N(WEeF&OsC{${+&R=NOv2-0<2F*y^`G>7!>aWoX@MZwHxFcg{3;g*0 z#VM|8yDmOP;HadS_aAj9SzoW^vG~PE%o}>lt2RIY1V;LGbsRytt_(Y=Xs;%^dNd|7 z)0lkg61)~{M*9k%D5bg+f?am4?CRSiC~$7m=#&AOhD%X=4@YXq6gBODV=*m#UkL!YqO` zP5kd8D!N3#Og;A$A6c%N%i}*kcW2-xCD$dSO2?(&%PKCvwU~wr#V#QL261b3&otIk z5-LO{HTLwu9bz=!9tJhZP=Ue<_ePw8X*hwZu_2{iag33xFSE{3fJ0kUcXsFL?y57l zc6ZI0*Zj*+obwGhkKX(W*Yp6^G*^^m)l$RLr?i%b?po4oIdX2ra=R_h{v8VqR6UY7 z-T!Em`W(Zbez4kpnVNzbp!!?4XR!G={ZaMq>PKU$?9Iii;u2k%{mN35e?_zuKV#Ea zRgJ#m!anA)hcH*<`)4t01)r(~j1}e>EpDq=iQY>Wy?Ie8C2yAsBpR(rFzU`~jcg3PUyxi^Xo<+p&;`>ic>h1!}8x&Q5L^n@I zYNI=lBLK?V+^|IiqrNA-Sjo`4)^X@#SWj0bOOv=~5HVA*KCtu&DZW3k1g>HH-yxKX zoPJ%+8o5(IaoX6Y-A``;)cu{r5RZTR*Z=un>*fROn8XxQiexxWgKi?Ix@rtbIR`He zbv&wzAJ_~OBovvS8;|J;kt#_c_bf6QQCON*>*9r?NlD{OiC=)BQ>Ls)p*n3+6M#Ah zVZ~Iu9Za`ZDwd%b21!?ip^TJ&dq(C-Y{&qKC0!NCIM9o4z|3tEO}4hpsR4vwu>ZGz{qO(#|NWQ$_HY0CKmOPL z`nq@r7I4s5xY$6Ar=u=jhY~~5Ixx#7%kLZX!aRqZXlE^9S&YpS)1)C-EHbbv6es-L zr!3)AV^v<=V;2s^t09EPKM|GA=>Q5H5r|>Eg6JEJHp1e+GK%yVzSR-R9?}2 z1^NWGm4|3@yp^qSrsKYhg1<2uw<$zKU1Jir_6htHVq~vt6lH6E_Bp+VC`W5E+?Eg% z2%X?0#Nvh4mw;I2jF4-u@Jrdyp7w#_$EkJ3nTsNwahd2R4#1f3$4y&q~Iej_=)7Z?3#tRw0VaAs*zTg!`Hv&C~RgQIkDeGvAD1y){;Ut5W zi^rP0E0jge(2Nuq%=Qd8q*;G}4aSr!m54zA-=uIM+DayfKM6-fUbu*SaON?lNRX00T$XCX%p%X)YNFx;N209lIe5uLB$Y82z-XwU0-WoU=eVO z_QYhO01l{qAC7C{nl1_H^9A8RbJ5{)2xbRlVmFtxb&QqofJ+g&WY9-58fY|T77-2w zf-QrzF5W006t)jisr+wNy6Bv`=&Y-@Qspd4ma!vQB1{{|e~()3c&E9gx~l7KcJ^9c z+m{`$<9B^;tFgPa+iJD9nvLD3r+m)4iVqygI8Wf>?^{W{oz`#ew$BP6isXgma~g!$PMhrC&=XCrrU9E4&1 z;jK#Jpv%sd{?PUeRd$PGIZ;CpaPlu7!WU9pfW`bF?jBvhDdKL*-(L`CaROIyoU1s_ z$BW}!DP(gfWD6hfN^Nc8%Kvh_ggb53>g;yCW@ESIwRam^p4@G0d(GXZ+G%fZHI&+Z zdWzaoR`=mOg%jIo5qDulBTvm|c)R5Vt`z%OI^K9@|M`f@prv7^F8BeSI?B@Vay7wx zF3!(OvvHFiSfZ#(3$%}vb-?`HB<%^uxl++!j=Z9Bsx= zVW-PN5xa~7$^S8kr9~h~WhBQV2@A0ZjLO!gN3JW6WoXlX4pkLIUd{<&NWTQJG{`A- z=`Dtqg^wWYTd}z`QnxCIGruEnjkh^JS*@j!)xMFeRuQ%(U(E&V01i|7{h0v@=VWCC zSXlv9R)8mD1;|f#r4gVc>wQ5cfR5VSZ0~wXw%T6X-)wtQHkH@vbQ>MNBexq}|7$k^ zR4?trTL4OXXc7H?qKU6t`zK_;B!tql8_yUgkUl=55x}KtrX7G$7fH2Xy2DdMhdpxRu<>s04)zMTW=Fl}9TGFs3X(k(|r}ar>F0yr}fAU}%8R zxnWe|4J?Czk<$>oD+8~WMHqX#v38GUl=8Wr$C3hV5Pp2pjnJ&wka99n#K8V`wc6FgB|1rpQ& ztFpfwLLjjU#S0ZjGWHkJ+*_@7+w*fOJf^}UD!}ehWcgQt1Y}%eV=z`I)P0zhLJWFw zF$~Mhw+J3*WS(|BKgc*41fe+S$MRZ+3k|2)*xd5^D)peq5oRM0)rkKK;ITPPxI*w~ zP7|O2BTZ&>FDdL7N=o}m8j#~3vd-y96Vi7`aUZ@o2x17eR(3dVbgZJqaDAJt)|N*h z$6lnaJ?xvtFvvQ|NTbOhfxdARlKU=NScIy%+*`;DD zEn<49Z074Wzl?N}_G2);n14NH=!GFBhVcKiiDy5#T+LjXiyxbtyi@I`@~%Ge8j zVj}dyyt$;v3)5VcRV7Cg{xwtJwJwBQ1W}^m$+&zhfQ&kxtthZ)uti~#mJn-U+FL@X zg<1HO$>k}UT-7U*pvlufS$H*j>O#rc;wS7U&PniS$QO6txbU##h zI`)wHshl1i6F*c_C?id0+4PXYwr0!sVc%@xaeKd?*|2l)amvW4Kr?UT`#7m|SY}_n zSi#Mx{g|vj=ea^|V~Y}2*THqvU`M^Z#+r8f#;6JfWw;7#B-H01!P}zha=P=~#HHRW zgLFze<_wD;I_(bBP&#G%2y_I#tUzI=S#p(mlv@CW!glZqc3y~Oc5yl;nzaZRwzw?` z0)Wf>Vl3WCD5gl4o-HUuZzB9L(S1giyaL%oU0SR+csmlQi3KR67aal*H5h+=Q|^zj zCc)0yHG43Xm&(>RJd3lai^p@HbH|(+f6mw;0B(Q=(zyI{lEzsV1UaWh@rlCxesK!s zov2HdV1F{DNU^3RU;iNc#R~3e06EEqR6sr*s32zOo)8KNLKP^PU|~1|YHi3i_$9XF zq1k5k3d2%@aJF!LhBEC^t?lz~d09^Mb#neK>^DC^xyhg>5Zr}5JY&QSs^wxLo#g}# zp3s9c1y0jM3fQfL#N~l;0@RN=C_08uAZ4zv{}~2LOH7m^g0LAz&TAUqU#U5Wu|OpA=q5MW~>jFCc-~ zH=P&rMTzw&Uy_Oq2BWuINsJ6X4aa#gQeMslOOW1zq0du$^iU>UNkxdAjg5d*aC8ax zq63ifyzhL4g&uX3kG)24LtwfgB_lECr7jMxWDw%X4tiV--94(r5olIn&Edc&_Wa{D z04o7S=yYc5l^Q6Qo~hf0hbuQ>!s)&^ zF!j(;bRktJT$WhAX2)+L^Pm zY#7>MkQ9(Vha5?Yo(w`b9=hAjAPwCzqF6%#*$(=f;1E;sIddCl=_pIZ8yWYN_X<@j z@s1IJ4idL2EV*GcJ%91;0Qi5Cz5LUbsd zXv2AGV~r5r^reatVz(FHH7{!7;=9%b-MJUvZC>D&l?9-#M$0ueLb~%@<4<)<>y}XCD{Q4}OgZsQi z3+G|3%XD>Pt!^ym#-iE#V0ST%AH^ImfzySaXJf=={a`}-LhQEWu} zlj6!IgP|NTZ^3sG+JJm9z(KO?=B*rwmlEq*P(uh-N*9b1dSe(ev0$tcDzQujyi|7^ zJ8(Tf$mQKHkg@_L)+~Y{*e=bE0~4`cOYc$bF?w{EC9WB0S~GcM6R#UyX^{eBlZ?kT zD;uzSj*cksE>mZT_&!nT+VWzz5(P0^ESKbQW?*&je}N&T`CS4aoJ#r1&&5CED7Z(t z&$uU60^dsDTgXH~Y$b76DZyAfrePA1a@COUF?Ko`kx*LenRykiypie#y|FkxO8Cew zqED_wyY+Pp^U)EOHwuGpT9V}wi~=I7J40X_iRLQM9C0tQDEO6P4Atjun8Ebo=}Ti7 zL=d!}q)Pgfr*IS{yUa1t#9~VIk0gMFC=wN^(8-1`P22+uO!ixB)XDBA4U?j zobVxJQo|8f(&~V3-Y^I}@)Qr*S-- zMH--_k{={Mrf=517^ros(sJxn@mSQC5X&`hd>Q>xh%dWC09a|P4;280ldR*^*{7U5 zm6*I(8mIDkg83A9@QBfd`wlZao*l`FY&C09OK zxsrsnd$imwVz2v3taK~e+{!k$vduk=ZSDaqaw|LBN?uyYOHW*0`kmR~?j8Q#;eWS< zfjb200LNrb$*-X}V!mJz#r~KL^$&kq4CZZjYtQQf{1x=#aN(q~ zAIl*yPeA&h89vh^aTY6Jm`L!!u{Z`S9KkOlqhNyJew95bE>+|Q{r~%a{-zc~b!+W5 zHa*6$0VsYZ%wq@=LPksV$V-th8lwyd!_K^j>HRduA5O%-7sqD^mgKACQo#IoG%vwH z<_B6J<1?5rc0{jK7>ty75zGE!&~B}rt(G^4abEMO5 zWuG>W2=|c}O3kx(;w*yLAnByZG8>tMxvL8GXDOVaJwPdB6DJ5`M)eJ~^ohqKJ`y}h zxl@=Ylt@S&{u_4YNX3bcz!FL6 z%4#Ts5UQ$!6i^qh(R@wPHUL0TIU%^+Sy?)d{xf9s3jPZpJQsQ{EwoE ze_Qx>6aTjH?-u^u#=krGcNhQe!QW;B|Kht%eD~twLfA$>;gdp$zng!+|IWX~QJjCf zFV4S}q5AyKJOcowbpCCv_$(+x^yopTK2Q9NXh{2_)fDFY-)cAa`imR}YR+zdglDJq zySbV73;+15odXx$5bD$`yL}-+>^4i&oS4_smv3Gko{4qw>g4@f@m+Hts1-i}%jO;O z58}Jle<;v$21f}E^pmHdW|$RiB|I7h%{k{&=D}X@S=onh6I;O)(8yR_wObp`qVtGI z=r~y{pi#yUk%bLT7d~q2w5w57fJ2+b(XFR&a(`}YP~9x>*r53dsdbDN@X+7?EDeZh$*8wTOkbOCjlyY;cxxKgR zZ8!HiUVB^l-mb4Y-gcwW@!LC_jYfO(Yloz)T-Zmb0#evP3l#w=weZwHC~4hMON@2s zspu+*b%g2O8w71!{O(zrr5bl!#jm7{6`NcZK;2;9iD2!(O;#!6PCOL?8 z)+hG@gBX#?*}^#%mPbWC;FGJ!N0ATEENLD^daVnO?h9XCsZfs)R|#kt#oEt|0X79T zkqEvoC6^lsQX@ddy-fh1=1~H`EC7It0Eu-dXQWTWzR@GBuM(s+ zA;p!7P1*;px3$XQV!?_}EE9$X0kNG}M%_WpR9FRtEK3F+N~mwqG8Th8?tM`5bEe!v1J; z#DF20m-odm3)8?eC%DF+&G`jH{L%yD(c&lA-fwwlR|D4&9i8T*WW;_@UR31c`P=8< zI$#RK0#S`6-8KQ8++{1SMF8KxBSG$>GV^g6&qSl?pE8F9gDPe%b7 z|EtL{O3*B_uDaA8{jHCp{@PL-V{?x{=;0J=`RJaJ)QH*0Sn(Bc5P`J?i~zEy(y<8T znAVT2mc~753y$StdVb-%rxtYSVUPg)Bi(#>CggJ6Xr5)e?59*t*QqFCWN z&DqhAVhV7T$|G1&E$?NGP#WY7$sC%S8O<7bmz_d~uPVeTqer5wUsx*|9TR)P++s%ld^Qp8=?&el9Wsp+QtW zfHT(=IE}}`3Aic3PrPz+rC6D^O3;EbKDptmR)8?q-sR0hA@^!_vP-A&8JumpowV0R$MR z0t!7$8CSC|UW0QDv(HdVNo+ko*8*lL_#J~A90+%&?BmIs^>rt5{q*z)_%3%N7Oua| zl5ynaQEO|fxrbZ#+571 zgu$HtApWSvkW$DcEBiqz20BUe?6{NDgZg>Yu8Tv^Wenw)YV2hH;#DpI9X!-yB6b{J`RZ#^IqU9d2~CEk;HxUJAsG~-=OGU^ni) z!Q#yM`G1D20c3nmzhx=B$U}>@k2Xw1HWc&GM$#rHLo(Q~Y&D;kq=K10b2tlZDrA55 zeBrWNara($3a}Hn2J$0^{KR%TP3gQ(0kC!N0$?i_0FR@aIR`U2r+_H086>&Fvhi%@ zS3!}%5a;>GC;QmUZDM<^83QL>LM$Scv1ZMwU@+(cAFtbO#NHmGk8)dlbyg^<>|jEc zlsk2buerZC%JpVle3)zyv#j^?x%v?6l(KKl6O<`rj7$`U171mEgtXcf1(4?*wjFkP ziry4Eh4m$^=gmu%jiKm^@z8ER(~23hd|tJ; zYPtEjr>-YOMk6k=^yuXLk;zB9Mh7%Uo8rKBzF=b3*T4CM69h3Ui<`V~>=^a}_AV1x zihEjg^X$flf`d(kJLX4B65FHmEF3Ay^A-74R-d3y|TX{JW4Bh91%g zEqc`49j$!R+Z1z;s2I65?`-Iiy3=!V9YI}^6WdFBBghLN{y(P}EL#NJVSk=4uf zburYw3@!Vin<8fq?qvZly5Z#^2(}RBhH*mT`$%2apt+zID8I&4hWs@y5raYaaM$Fa zI3$}Ck|LoG(0%4p;qX{))+uB{NDNO5QZa-i#=c_ZjLmNlJAwK~0uniJP$weEB#Rj% zALwgn+`ptGIF}_lQ%R?t1$WFJu_j&Pu0l&fkc>h(2D@*Pk&MbD3XCFv5K-6AhihzF zaqiY7v(_*q)|zV1>0}CD{fkku1?Fa6=bjd0l7Tmy1deXFGAh#lPg}#@II0h5QfE!C}|BKG!3)O9po8vOu_tx?C31@x>=Iy zq4{#nDv_Vyiaq#nJf$M?DA-tYX5R`EVJN^^I2LIzRJl_&V(GtXtPt2!(7(>3gVHi{ zR>xx`LpAZ;CcfLmcbnT2Nhsju*cXS#@NXUMQ##QzamIOoMn(Z446ag~5-`fTcMa#4 z8RNkDxA_#Ef6J3Y4a4jECS}Pu9`?nj@;iSubR2|MwB)=yD!Xk+8Zz)kjt+9b5Cy61 zP$TBqpFy+&SOO&A7u`vik~oX-6j;LIEP}9yeUWvtD9t=f15M4Z7`|bj1$PzA;fIfJ z-oUr#DgB?48hJM)sv9e^&WuR=1|cvB(%`BPQ|YF8#aXm3{?YwLfZ-(+o)>B|0gC6} z{r&Hz@%r(*)3bwjhcABy%kgi&ZE)NE-YL?V) ztG(UY@p^u%<+WRTzPGotExoPY-tJ~+v$fmX?LNIMsk>889r6n%S?0jaL61x5rTiWq> zpB}t@Gvh*>Jx+++tQ_gItMClN3n4y@vOy9BqO56Mq%yhWkVtq&_Il(m=!}t0V|;q~ z8K7)<6exk+brcF)sd`#P<4oH4 z9%^Z}f)^`HB9b%~Bpk2lWB)_VBx#_g1G=B`S@lEDx0`cjrEzXA9jKvol0#tRwF*Pm z8SWl~`NrvP^NpfA8}t*uFn_Du?s=t{+1sg7W|_jm9#byq_*>gM{+_q(clNyYcBkic zc6)8F;kUbcZMC)ARL!R+m(0I(4H93~po~5-r|vQY67f*Fl#+YeLDkvC472>KxZ-eE*cBXKpuW$_Wfu z5JDKHaPp#aGBS%%VMda8pAT!vi|I_*RAkc+vU>>|3n3X_tJHiU-tx^f`OBZlT9wc* zr@0fDA4s6xqX4kOIvF9R%6{I=K^Vdp{5pfYN_yrY@DF)%ZU=p6FzjHbEC64JTjL8w zuH*Tb8?>%)Y@R1Y;Epmuqv0Uwc81f*ft%UXVnomv=o~T+acIP3kSbg~QDU<0>S+p2Vg5^uyWN z2XVqC{H1|2Llf*5AU8`_GPV}4#PbAvGj&&eGbA}78%Ql0EBqXOnp-dq*ogrcmF{@T z?~Zr)pujV&&i4T*fYK+vaPZ?nDymQf4LR;i7ozrar4Kh+KD>Hy!ylp}3#P@njOJJk zc7WyYOgRoKHRygIuHal(szz3Z_;P@)x=q>K?rwQ7fdc4!2N;VW_)WtYZCIR!LjFzhr+b)) zDKaUCGnQ9~j%=E!uxEtS19xl>yIO^5U0_)PD186{G+q+w6!8lzfI?xF?z@Cfd7AI8 zhtmYwx(6q$&=l(UnC@&PpXzcy?gHCK^LO|2@^E%j7lceY0ez?dl+IT{2PBi5D(D;^ zx%G3LzK@SgU7}RRDHbX*bsZA?1|z0S_)ot26ExSwi?K;NtPw;TE}B8+6$&7IDI1UT zeoUqjcJaNq`1{|*cMwwi+iw?UC0<5;fhGhYx*v)pT-h0WNPvu-qWUPzlJA=}@f@Cb zzGf(NV*wwU_s&s(hRb(Q;f_-IA}-otrm@6wEoZZpI*QwRUr<>}tiO?3f1?bcM8((y zjR8DH$W@upk_SCeT})gL{M^aG zPh1em29BXtiUmZ1q57MS?1oDDGwctG+8(Q9q$2>Qp4dSs%I!w>y`s8@%J-{SsZN%R z_ZgKN(m|)+OccTm4lP=D6g%wVLqy}%<_3cAL9)U|j~m~>w%1UxEDDY*E%D*B#5s;? zr4l)hDEFuuJD-9076zs5c7&kDW%}WOgBjjY@^|A=F9`brh_u&1tM{yqi?;GCUB!Qpt#wVD(hw;Z+ za&KFVE+_WG(JQ+~9N)+nqaQ=wMo)*35P>@aPmryX_BIH0EPe4R2!oNn3cBYD6-#UG z?0JT1C~xHVJWCD*N3wAZ$?=FBiv8q+SRPSVcQ2t=%+iW8PL-hm$>Iopka02yLUGWK z<+WU_Zr$GAu0y`*+jIN&XfGGYJyA0BQ2E2UfQEWFYZX)ddXV*E8Htm?AIR%qAwt_* zn=LOk-BO{zH1Bp|N{!o|*yaL`dv5DauKP!EW!qD=a%Qib*(+!E6EaiK$*#0SIHKkk z_FiyuGg9^LBQ(8tvZh-sVng>uWbZOfKoeyR@CXvxrl>@cL78 zYXjf%0EZ@vf^>`@H=gwsmBl5YBifCYrR`VrHrth~Qsk^8bj?QCtQraUe#>LIAq^!j zGgIg|zwc#oNwvRON-)Y*Psf=9K5r>8^sO4M+b>WBlv)P!%%O`_6ZT^1A5bz_>`P+o3vHpES~2FDpPwfc8l+ z47lH*H4j^xUP~7ZD?DZ>BIkJdaQ(25Ckz69u+!iU9;_mLvn_iv0Y4=dcQ=(mjm?oF zKqsJbg~4R*1#yxh8Ma%C)sHPFfDS)q0XsFDCueNy>vl5N*PZD!a@+e6*TxN1FZJL? z5F;6>upoJ@uY-_*;#eU=EN__rrOqZgL?|wV8$qfVHi~64_L&x?rmX@l)o9E#I}fqj z7=B~iPxt+8vV++|FTgH%5eXD9H;Lq|nikefvpG-sbRQuMf1*@@sP*-KGHX%%T98Mx z7+26y{UC}MkpxIIHMg4e_Flc=oJYU#=!tNtTyS?o_wIw~(*i&A++uibZ z)n?CYb$d$fZTefAe(UK;yEiS?L*%pB<)@m(#J`MS@;jG5Z>LQRe@)YkXYltColwyI z?8N#%ot-V$1%=~=1bZ{`n;cL%303|l__geQ!Z&7mpR9`q&64m>;IfPi$%&QF%A($+ zoaVbbld!G3`;u_pSFWU|=t?q2YTgjw(wV|Kv@(ZoHKtdXWkP5mz*HQsphaXQjz&>I6R zJL13`JAMkwFmi`a)=9d9SY{m+FXpY<+}-p7oq7a}=*38fmk<>eH^-R|G7Hc-H?E(q z0?(RdY~*0a)&Dh9nOdrUYI|$Pi>XJ?^yu|;>`5fb!yXN3r?6iv?plWVrHR=j$ET-M zITN>>Ve?BRw%OR;^I+3j@HdZ%@N^GPaFAd%7U>}oP_!hUTH~?8nOwkOMJ;`ysix-8 z%$;irH!2$p0BAX7utVeou;cJ1bJw{*+rTrA*`CGeE?_e;-q)MFMVsi-Sj& ztk=@`5jxJ!z>RXaLrA4|i@#6kxAfL>;w!nassKw~Kl)ps9Bq&1(#FFD9tTzc8#emb`>chsty#fJaxIO4O+!fA~($PqHXW7mY zl%sQf2Z9WxZR^B7`c7Tv-Ox!g!9Awr5>t)B01HhW*as6Dx;k05M2yJsPByYT+k zEpSt?I>xGRBm)^Mp99w`fH~-gDNj8nGRI8v=Yi%p!qkCNY=A%98#c%6xz52kX3q94 zVIVNh*;I6^f$&UHG#M)ebwUXgZmJ>9L4v2rji-N9t5$-8^w zp-%!M_{+j6SY+$7c{Z4BZ!{ZQ`knfAbK@j?}!lg8^n=T#;YKs_>>w|Zd9)^KZ7Fu2`xU`me zFNat^lM2V5n0oR*LDlqFz*GkWz;dDh@wIEV)l6EbBq#oM0bz`$S!y+J^a5-urd4A5 zb5MGu5dt6U0*LofI9`}GA6vqPpq&ND!L0-ORzKERO>s+$ zj2}ZY)0?}b=b*t}5UP2$q#ljuXm&K7w?QoDpyoeJBsJyd3C%HcpD6B)neu#HO}0pp z^7+j+lu))e0Ba^@j=EqzL8^uz=h#U6G?q}znm$Y?R9{D-mN#!%pC9D;W~`oBa{$8k zjm?4{Fn^P8zI3s4>fZcI$VCf7L)#K##3o%fi1+Y=9LJiIQ5cqzHNG%57OqpyZ zFvXS;$V!00DVP5YZ)UWu{4?5@eV@`e%1wSjQq6%s49wy3MR`Q1YH>x1ntK-|0y*6^ zFh2k)i)rql-1IOAWgL!Cga!7bLLX-%jWd#EK~IDICCH$5!}*&n1eN!hkx4mQqb5di z5T(cgN*t3rpIn^0(zzcbIC7*q#u|@_O?JY>q#Q_XLIf&pE;D!FXo1q@mHPsHgLrXF zrH*uh*v40pzyx1Y(fP?YIx5sr-=TCuT)COln~4h2#4ywfP`9@#9cbTd@p=#?Do&TU z(_6gLH>l-X0lS&nQje`?lh|=5J=?Du$Jec6)6&|9if>vt(XvdILn`3wb>p(U6P##` zz(hZuym8S);W4O6YtA!tUz>tFn(GM1{sj{?5sPOZFgF*pZWL@Dq`PMO;=(Qz?w5%Z zmu)mQ_->|ej*l*GKIFU^@8OVB%eOt`=#G8V6AnqgcRAkui4lC=XZy8E7e3g1m99>8 z^{IY*lj6_KSw=-T94u~;^m>4DN=b?#=RK)mgbf4wfRUFZya*MOzyVw@$+`Vl0<4wu zF2rS#FholJn6uLh%}f``)HI3g1{Mkj{vP;eU)@?l0_KShmZF#H5f~^?QQ)Dsbi?&4 zNAlG(QcwYN#C5`hhRR_MFTHkE1pFPMdt#MjVT!f_4W`d*M4?rLR2>GxAmv8iz`u>& zK9rw>VKx*|208;A9nvxK#;@T_7D<8ZcB7Q7`2G{*`>k1g|4)F~ZT1g=gOgFmhNXZr z$ri^eo^NpqCYRGGKi`4NLKvt@HmGA}OcQ38B1c_aVvlo)+0uuPsRU58$DQkHa^UmxicGqXb8gt4JS*m4IW7V>8iV@wDJr8hzpi&; zrb8gdw_LdbR<3}RD}bZ}zRT`GK%kaaHD11i8ZWcC`>Y}_vx9;kHBn)w+t_I~yvDZd zdTr?|Z@0JUdw#3Y+u4=f9bY!S_C$r-F8(8wd6~AeO7YjLIxl&C1pW7~-2bmUBzZ-4mh z5C0!fO9KQH000OG0FZ>IT#D1(bvP3M0F^iZ03!eZ0Ah7%Y-BBYZ*_7lW@&PBbS+_N zEn#P6Zged+GB+?TYIARH&0AY@9LIHjCV#~d>=J>koyE@H7hzDjGHNDd2K8OVgO4)za#%}}|p zN^`VZd+^2eYuCQMe(l*#HM|Hl_`(tq*$a zM0&D-huUGUe&rUWf2AfW@+U^_-cfxVPBh2DSG#F>YXgcb}QPQ0xY&wb=cH>$mY53ycf>4=mgGI#w3nrv(15+GW<#fRuf@Ai?c3Leu@<&94h8a4r zjv0r1??}ZRfL80aRti%GKf^*O(Q3iAMFfK!g0z8T3<*j--#Jpf4!~_G(i3nwLaX)d zH~;z{@QV_aC%V;wq;gIxPl+=NVD?0)+1`-?0B8Zg>sP;j`v2^`e)VtGZ!t&aax@?m zJYq)JpS*tchmF-gLk9)=wURls4jiUAD4g5QxwRyh0zq(O7BX!>P1H)Z_4*)-(vzPYsZX_mtp3@CRI@Y}YSxCN5-!TLi59woxi%-6)N}taQw#X@a>os>o+HVVK*3T+bQK+<2QpT}Z>;xsZ?y<65z{&-;>7Vsd@Omt~r{8yEA0cmw?@;+Orli(3CXQx7&5OxffQlRaG zlh6LSDnQ5SaLoJtsMn`dqdVo%v^yC@aeq4P_NG&w^!axI=6Sb$dGbScu#71i1uBOfF0N7I;h zqd~uyMbmz79Pu9OcSqg0JL#q0MXjSBRIS7Bpw{vE+`l)tidqkIv{?HP3KtEfvNT0& z!61{-TpP)OGsh@%aqI+rP}0hFV4rg+G({l|_@X4E7R~}ogcMGo^||Xh_j~=ve}PKz z8Z2uEaj3Nfp~}hyCqkKK1V7VyCJ}BkiHb7=+m!JbZ`zlFpU~>q(@gq;?b{C@Ht0HD`|+R8f6eYWy2m$&HR~IM z$+j}s0?WzgMMpNsx$~;+EI>a^-uZl82i~t?N(eIEmhppKPFuu=tNRtPS`{I}@eL!y zq#4@l1_oZXL)~x(&3w{#F_z2eHBrj26YZtWQbW%~57G;hBD&0I>a$O6+6=N0EM1Mjb$hldyqjpdkw_*z zHTVeiyufWNg*CLO-(feYjU~9`Eib#PYO?4^B#MNOP5PS`JIA)`ou@jpM(2$gF+>B| zqlr@NQBA(?^HOC$63?4nd2enk#6{^|-HD%P=T`e+V%>tzIMBNa*{o1UI)qW?Ijn1Nr{|K#>8O|^t9k5820rd<&*uvtPjd)PBJb`rzZwXr# zy5%vpJKB!D9(M#f>R zhD+`u&H%RUXX3NRK;er^fllQmjaN1y>`9Lf6!)yjUoO)2+3A|)Z=&D+qV3J;#dt5g zqNZgDsT}$*J)X=8-kr>9Pd!xTS9w!b-5oC&T7a}DvZ6u8{xt#OQ-x~pukn>PH~6X* zgFCSOoDbZ&ughjkGj?=@Y!t6_x?|vJce!ucwKi84|grF6?ilE z-}csuL%*&y;nAAvFezlF?r}ivEblL|9{Zyitrz}+8v?iNn~p_&aM!Bu121zoE?MiB zVv5QmKiABmCEfvOzITirkm08nQF!k+bsUBArgDl0*&MFdYYlu~NrqL%ccR?FbR4E~ zcyfgt3v*~#9a9;&uqrZ|WbEs147)n!B|SO^`saCeZ@tlMwg4QifXfY^eGIlrUkub^ z2}kVy?qVwMU>Aw1kcG+(r8g=b{MU~ChuX$MPfc#8xI!JO%P>@Dm>s_lxFcicYBVv=6?b$!mcG=9?0vs}4iQDd^txm>& zy%oOffDA=G3a-7%ct)>J3AZPG`6h_AiX2{&SOrAzjkh-hpJ}_EjDARh=2Vff#cyP7G-#A*@k1t48{#qCuISQOr@?Tvh)DtatqBA3JZNVpRxGJt=XSN#jyGyg4pYNaEvI z^haot7hh*>t6#9T%?3Dd>W{KEZI`JU51v)9i!{fc`LHy>E8BB>PHELhw4Hzwi0<=% z_oH~(vVzo+Ip~u13yt#}}v>)D;cQ&FK22(&CbYD@2I- zps1ETDtbGMNh*v;iiooVRo2XDwBbC<7s%1o?`PQMnVrpsF-tN6#K96MpX2cz9E;Lm z$<-M7W-LfcXJ)ACY0K)3!qyj2H7%(g#q&Vtf=sbB*W4UV1xnI#VFY}K`|D8L zRqs-$KJ7^XFG;Bo<*IX$iVc_m<*F1ZM>X_l5p41YO`#aolmjk&1ktObsTrI|1t}^t z@kB0*t<<`0asy?Rj@jn3n6{k-Udb(6`N+@R-C!$0Qrn*zTb+J&qcTEL}`2;*!iyp36iK zk95^5hy44%)J+<5HwLBAFX`lvMM;11jR`)Vo9T~ZP$;ZX0&7>irn zi*Tv)@Qai81)^CZ9;iHT`}#vbk3DXHp%BCk1L^gU`$QlJuKCN`YOIA=H-`!T*o{(! zT%`&kQBW=qeL=Z9>RWaH0s4sQBAvqUMk~mB<=hdfrruq-CP$bP-%p%SPQ}uheki`k zT(FqfpWA#B7(@)5r6ymMaH>|@j)?3I98+BtV7poXNs!6*mzf55<^VLIT19BF0C*PeF1`e zBuFz#QK0pSaGZz5;Tn-kmyD;YPT|!}&{kduALFFH45x1p<1XF@nkdUz-LMuSpcZ*1 zXBi-am&cT;5ZnaYw^g4@7eaw$&N<5)7=p6^OI8N^i*;5F`Y@LL@SI!n5&FoLR~DAa>F=fK$2_)zl?O}M)Oj$9Kf$aXHjmcfjBci%qLa|WIKjp z>_nR{q@5y~I^rp8hkcZwlKIv&6rCF+pe51Tbx^FrM!g3mU0Ix~y?x@bpcFpk&ba!$ zIOsSNfN4jNzc#BAZ#0Fut5p`TcX|Xvah+!|VaZD5T1CnQ<$?&~rtFBt3hYcBGk$?Ve)K9wBzm9#fYKT_~`TE1H-FS1|8Bmu21};q2-nL zcRQ@%CSTXhxaA-F2+_FWRP)i9iCY`_iSN2Rsk@jqEfwYT%tWuyj<8)e6 zn*t*g2{~}pBt~tQXMptZp4ij19C{7h49jI`a(amwB8CcLsE)!_qCx zrx#~7X<+5xqbt)cN^HgevM0-GKevNa}7VQJZ!f z8v5-=gM}4bhuwWMnjlAxEW>#*x8#BXB20qf1R0W}aCw zd;c(OkxxwZvd7oOYiqbs(3co^PeKuOGN+;F52VuT$1>i9#kz7T6nsAQrHej`#^=BH zN0}==-|wwhwGiK@IZc_*0h>VXq{+JvJRhj%b~p#w!%v@cBGWJLnnmS7UQWN>K@o4S z%ICCs?RzaVi&{M*?>KmswtBq^Bc2Y`O0&ptBS}$JA(&_L!H0&`AK*%<+MSF&E!!54 z{Y@pR!SY+JFPF26QQ3_r1g^; zHIirTJexT!>Rc~$!AMbJUvS7;Pr_De&JG7n7*LyJ!EJ+>_uW-J-5MtR7M2bwoJFLJ*jO!b+}9TjpF{zXWU^t= z)#D;{1Rj9a740(w^yM(HP?ywGPT^K_pB~oY_NZT>R-dTOW|mBYc3xRdvZxly`D5_& zk!`oo%x-q>wbV`l^cW@S8Gt7Lf=7gphn@4|ZSTySm118pf&PU))++*njG?5*a4j)k zP6&_zq9}XM{C*lQR6Z}Y*FOzCE=+E|Q81Gr=}b&Vw?bQw?Q7ck>I1E*@8&VB5%NPm zHp_*VuCYH4?BT48ZAHbDQAb_e=ps80|K%5GXGntO@4rAhNV8w&wk_?b8bxM0@3Yit z_-@>z`>FRRWQE}YP2#j`>GR>-P=8yCL|)4E6gbcR3*}~N7dizSNqmQ63bOcy`ool) z|NS!ycjD`%x%a9kugcQ#ujeQ4yDQjM-z_*fu|H-i|WJT0T#`h*-$a>tIz;wio=1C-~#+(oP>0-Bu7w05w$JdVVQx) z>-LKm6romKWdA~5;fiWK_xEMcu}bao{^9~zO5EG>XzZCCT&x{3#%SyZB^A}6(Trn-xTkwmj~(3 zrG~ty#066DVHjx|Ij*5(|6tTRqTx(|`Wsinp7eJaHJkj4+1CfmVD>+hH5D91?CV9U=0B&0LWXW*g&Y4p^gB;Jj6%wNo-1(EX=@*$b zxQ;6p>P5=2_@I>X&Aw)D2Fg4K;a7}S`{wTsmj$GT?O zdOYi2cG?WiA@Q_eEt5q^cj=gBi_0WDb&rh_w-sUsz5H6sPO2VZZ|qK>Kq7--aO3$d zS0*lt>7&T~B9}g5v@=AQ0}sO(bbjICEL`YFRAs4LjyO$)k3ghPNv`p4Oy@MjG7|5& zV%{%Z3OQDbaxxilHFCAy9;Vi6WlTfmD16 z^NFx^NwP1v;c!1r*l{%i)f#_QUaV#4)$R&`6@oyrN09D!iV7(e*a}Z2EBwv!`@7Z? ztWP$-$($9zKH5FP`~-pB{5R!JF-J7 zs;W5iQgR?&<@Qvtokg$=teSj-zYa!I>Y7+>UeFDN02jes$bD zXRYiNxbPPsF=_HV5jhpHx_^Z=^9&P<2$LC1w0b`neZas{P{+&^JypgtrE4*niRpz5 z$-|UuC8=SUda$e$YrLN>zl+6FTWn4sgM0x+8j|uHP<_{e`50unP_VISF<&k_vswpO zN3bM#))!g8fH4U^gpn#Hl$r^jE6m@W-s2RhL;gE@2?m)jbfgqYX^jDgA$3Y2J<)`P zX)W5WMla|wS}j`5b;tbB9N8B}YQ)EHZMY6uwJ}w5|#2Ta4&DeQxj})I`mU^MaVBCi4~Xfz1RG@qg@3-f*Gy z()8gfVT=3_)S$!*k6r8$`oYvRKDvKWP(J?I2}H^;dOBqdBU;LqSnUZ_)6HV1#YZIi zNd}wzK}MS3kkt^cF9iw<6Q_22W=(XP!e(*HJaPT=B!pUu@LU%}KNM5~y(lh^J?}>f zL0;3RC9nrKlOiygayMPpcwkIYw=Amb?i6PU#$QU(^T%LZmHlWilu@rG-Ba~#U8AA> zTnk+0#B|TPz=yP2rPbI#qD(kF+KkmS8j|YYso-6kAR_eSLq#u`hM9e}2^`x#0~J2$ zU*1+SGyxp&y~I(}ZfiqkTz-RVxwy57O*1ofJpm0$I($IBL_gfRp}&=grC?c+o1R1eL}bUD#S17dQ{b$`Ovtnh!ai z%IQYLtDuNul&Wa=Zq#FI7rGNP{^b9oaeP0+m*?Rzk9M$nCF!> zDA!#pD)79MoLW~;p|mYXF|!Q^nFmX+W7Sr<9g6{&dQP+g>+V@7fwx!!lmaH8+@bB- zZaPuuro(&6r-=$UvT1N;BYo!9GtcF;HF?aZi}gshHfdE3hQ6Pqf!eC4AKue&GNs^R zT10OGZXkRm1Cd6@`|Oe+r1g7g#gO!H-s07-Snrj%H?8_NTCyfkhuPK|UOUotf4|*zk+bTOa$m;^}+YZC8`C~U78-XF0Vf&;G zS3;NuxSQBPLyruvRZ>-5#!UNsYWUgJ+45DbcARHv`Q}q#)rUDQEE^yopS`NynlXu zxWHUh6XC!416xJGlxcNsiC&Lcq26doZ+U;?le<_78WYdAqHkFi*Qf34T-nCc0-HVS zT=`xt;Z|(LxXC0m_`N(KWl>1;i`a`L_k<-TEYgS(h^Z)VxOn(Wtx4h7Fgk#%!8y9{ zfYK_2^5@?~gGo=!H)Ot7NyDr*@Q2a5FIsx$f6AN{4)6C<%)brw&1xiPjfu*jv7%42 zp}e=*KCZqplPy*cDW1C`U%U$lBX5854~n3oW(j3rJ?`_+ehxhD-L$WL19i91A=8!b zthqxB$M5x=?I-|M-7-M|t4yuv+XA_g&6c;uG}x5`rG1X4GeH{x!`$R$93KZA(?*N6 zmMNEq-76d*q&d+m$R5Ifbe{Kj?R?x{_t#@@`MP}ne%u_thx+#Xdks~i+wJxOVr0^@ z{a%QI%Bpx#`^P~;%6e#~R$A|wyqWjOgZlGagy~B%c?C)Nw;ctX;=0o-#SAGu{W(v- zY^cEItgGH3bZ?!#(`2u}DMo9Uu3+YpRRX?*7=fLHdMJnr?@?WTs*w31ASgIrTgKtv z$T1vIYrS2B$x8c(&V0n#2)!xS5>n(qQNHxa;lg zi*Cw&4kLzVe0`EAEZww}{1aEFr2@Q;%#~Q9rmiMMuwWWjtptPE^&<>#6O&bc}1sh+s`B z$6x9s37HmEFX=LJ%}7I!OYEdtK*kB(>X*&^qG=;?ZG?W%vQgk-=kI?xsPnt$R^)OT zpm2CP$k$a*$w^P%N;}ys&Hu7>1WPJubk$_|dKq%(bn^v^bl3}g=yc*7?F1>Ep>m7AQ zi~!FP!TuC>Pn%=!#||6feHNrcWsb)9mo8HZTL|F|W>i4s>Mn)_6!eEU8G)4AJ|}P} z$sKz%|BtyO494n!RuQGY-< zOet2%QvnzeLs6pZcQu3c@*3iZ@^uHqB8eP7KKH{M` zb|}ZM#dHl+7Ne*h?G4D20`5v@8bC=F5co7++L+RtkZUiCi^Ws0-9EzAZ{C#fFRnyA z^woKkBzZ|bPqqOsoL6!jKL`%}40aWdKo-ny_(pkymBtR`R>n~I9b$w?(Uld^Z!kwVQr1v>l7#pl1q(DXSeJS>xQ+VGJ}?5A zwmWa?VEHur68h5c0qVM*kO*&F7YDsVkX|+tzjdUMVh&gTA4m5y&`XcQ!Mkq-0A?c+C0K_EFha(&bRZ>Yag2kSS=sUAfY{1&m zjnT~8PdC+A6sv-krL-xs5D^dX?-R&nuX!$Lp-(lhynI$k@-z0i1kyevh!X)uEf9?o zh7e995Di5N`eKbJzveFe>=76@kVb|rsIPcGo z)V&1F!ft~y>XoMf#X6vuFKq-a?4(N9$+?diq6C8L{9}(CxQ5uaev{=Shb2FaFw6jX zA@v=z&d?1IL8szF&LP~o4VepueGQ$1*##YlE5lET-=5(f5(Wh-RT!#|Fd1@(U#rt* z3!+N~GPpiRdt~3_jiZ%m%=CE(fL9DaK|@9o=Q!y>J4_?(E1(aOpO^J)qbETsL3wEF zR_IDIEX_ue%m*U0H})sS6;|a!r~;7^igniA=fSdadx&r$)yYQHNew*VlhCo*swpwVDqlFc7ym*14Tg>=&V*Y=hz#CyT} zUQNZ_jb9VVRaVZRl(do+0M z?_rI=-~WH&i&`5$LJ851r(Z(7IYiM)kyh-sp{3q%t2f+@5 z=t)E`be;10GX43eaY;aR>eJ{*ZN|Im=n8RG@Mg~Rwy|@%s58671YEcb?2<~mcB_Y+ zt7+j!i&FM(PK8v48927&3>KYj!MeAT7^KN{HJdAc2<{)CPQrd9sL7LJm?8=kNi43O zP_kFKe0`lDl_CF|s(Q58eIP(vZ~NKPbNXv7&3{6q8}hf)OF@|$)h2;14e($DC0U@q zfuHZ(jj_I6iaf}dn(i(egmP_&+>zL3oG6+yC?DrMoAlnG-M0Mg4|Q(WABt3cwcn_$ zl$K-qyVlze9(%JMxqhrt-a7p{7&cB}prHQl95Mbi^PRHpa}8u_*A~jY(F(}=}O<2 zrHF6{0uef>e0Rnw;g^&y2ykF?;_LX!Rg0-O`#8+$lDtRcK`-$f!O?P3SDi$&wY<<*UM zl7KgR-81N(rz#jXv_F-e;rV*1X|XZ;Gk!leq{!Z*A3`rxh&*kzV>m^NyI`YPN2A~o z5(Hwo=3iYnCWfY34zvw?*fO6q<(GM>bd1*;`^EU=J7n*FVX&0PEtJLWJdOb|c-yJ` zXsk13g29DDXlX@J`l#?0xyE3-Xb5up&#({3_cPg-dK~K5CWW?~HXlxF@{A zX={<_)X!eh>d=Bk+9gd?Ro$zi^Qo^w`Sd6YuN1@akbNE%eiWxbo(R$9cyUXi%bhNf zTC6Fmo%3fWT&%5fiC=-V>to7q_`rDJ6!6kssE0|S2rc(59$!3d{;77f zZFOJWb26Z4;$r>5d56!hbUvEMV(<-Tn{L~mt9_MI0IC|W$&rXd4*XyA`}Ma>PNh5E-a)7|QcBt8@A#+Y&HN`0Lj^ifa{2>Pg&2Wa{C(TNBJhM)=?hF}=8EMv>*w|LvfDNLn8Q$!Fhp8a8evv@ z9d)xF-jxkQIbg030ePitW{I+)5z?W~++gMjsC_6~R4dL>nQm3hLZ!#adV^r9JA-bjC}2B4ngFHKN$29D=ca8I-kvwXo6J9h3$rm=!f z5nz~H`~s@wsAzN>o-`xGUsZJqP!jy8o8E@SM7q<%NWhk7`#Sq#+qx>LZT5dDO5jUC z!k&SQfsiJ{7m7KWRNo98zg)@*>4MiPVv!>Gj2BT4@Uo#lyN{$oP;xkIpSZvMyJu;Tvu<06_5d;)pP zpe3t^8sFi8wRz`U3%rGbbIr~?pTWi1+7AA*mr~fxwm!HzNcKw83W*1+JxQ2zIBD|n zzzwAkO~5MidE-_1(yB;JdKS(8()fB+1;*%2hv)%kK27nL_E*$&Ky!NeB$9t6E3@b1 zNdI9ON4a)rYS^osDUhkSCgLVE z&lT@;oG5)!TCGEi_DCBWeUa)u?gOmKZ)V5_gc&Nqfd@`Ug^>$Vt8;%($I?~vHdSJq zdtEJ~3U$1WG*6>wIo8;AlxZbeyOo`mj7(YEe_bq8#Q#=2Bf7w8Y9}~U_-3oxR*YK% z@(*>6HwPRVKEeOZ-Uq{gW4H;ODvAVSh-$p_SBzuDcN9A-VO6XnWl*6(k5uElyvUYX2sqIyxybucOL2IgF_1GR5Xu>EP*s`mMu6vzec(Xwr{5m@c? zcRTdIy9UtlQFXJM!(~77KtaExnFj9dc2I3wtD&;`ci#U4{NH1Wwfub#6DRvRhz}|$$+Rn(pn#S71z}Sh#&BEE7 z#@NNi-rBt$Nm!Ky5cXs7Qe-N}P3wc=b>gIq4fZjwuQSAPON7D5al#_)Jx(VSaURqV`zj4*{Qi4xw+fYUkg|FsrvRmrNRT^8G+qS#c|S!`S*Pe=5|C`YcHp+ ziOR;Xkd)CS0uFPr&3) zjr;SDnvAfD1*yUaNos~MKJbQ`03I?El~b8_5PTVcoJh+Ox?xO714$0yCqb)4>dn880it=&&GaB z`_TO32%H)IL7>QrfC+sliUqM(DZ-=E-tLyXqfUyrM0-uhQI$5%tVZk55`|2N#WYm> ziG>a6UBid=%rUl^BKrc_ueIaG#hDS`GOmX8DvWewk)P|Xr_ZWSnfHx0a=gF zL`)!KcQudtB9^s30OgI*VQbx9nnqgf7M|O7zBB#6JjBVujzimEi(hpYP`Ehc@nEAa z2fSf%XDwx|qSLDwz$8*xuG^-a24xYqurqauEG;UVzAz*qT8B0-Y_NlYw<=Ml80F$p zHf|s>d@}gC?oTzlh3mz>>0w{v`FL2o38mw^!M*Z%N8t5ge>?yBrhB;Leg7`}{8w1f z4adcw?!j#GRNnni(k*MpOcOId8y&+-+28@I-Cgi=4f0){a;&6TuN3!mB+4lxZa02~ zVWjGRuTXRr-c`%`Icv${l$%^N8X;PmnrkSE=H_sQi)_sLxd)FfV!*pSIUZNx@sxy$=6$# zD~V~EU*EebS?wrLNlibTsCIAQWThvwsgbxyc;)u!c=cqyYj>FObYl2)rS!K|LXWrr znEydGBfhW4D}^PMJu?bjtH0$x5!puxY-cTOBcWN{yKpTQ_f*w(D*qjcSh z#SoTcJ?5l;PIhklC}(+{x#9v7mPjFyMKxqwIaU|O#tvm3wm1leCl$ErbSRDFA%!;I zI$NYr8GdVOtq=2*L#rwH!qm}LzpjtGzPux9!2_{~P+I*>ENuyuI8d+-O!bhx*kpE7 z+cRUF;YqaLK^nyk>fDp~H&nPZ?r=c{_`%vM=cQ8bd#aL`&2cH#wLtTG z+H|+i+DX+rJ>ElN+26ubsi6yyvYVtHZYTfw-F>A+-*??=h8$b&oIFpLO(n(#S}n^S zpdYqMub|Ki!l7xDZeBP;9-bKDHpwDaz+l;zn&s_#*m57Nq7O`dSbv;Z*=vEtDjH`F{5oSezXbyK5lZ2vYb`szmu`?B%+kLGwKcg zN|lLw&}+k&=Y#se?(7KnS)RH_48wQ@Y5^hvKddXO_LjW`U+ahYu8lae7+@`Cw0QCH|p>#?x)FO^I70iCpOnNA7mnYX}+jYJ+4P`7KoFRvZj zt3`8a#2H~H*YeF%)78YzLFJcH8Kr1{-sS;Ey{yEr_=ZQrcgoI|7Jd>J+0JN^Wxp{kA_uM<%3e%60iA0WwEB_%pnZt$Qu1gc26QZ@JKMt4z=4bzL!;u*S z3n`3qdSdTE`y8q52Q=J8FiB6#_T5Oi0BM8=FMaQf7Oi!yR9YweyZdF0%MkyrNBSe9 z48mi=ozv3&A;BFQ$1%`Hp4%p{uhbhhKh8&>3AH-01Y>4@))xE;81oK+rUL0?K$f25 zMiW$4q|zL|lj2zaNG(gH>UC3IhtE1on*5lAPT8u*lmm*W=$n%}l|OFyjWAt$M@t#{ z!#AJgi%Z!*TIX5?oo^pxJ&^-}rWTFa0ukwng!}W$7A0u{Gwd88f|;u0cvIsS!+jlI z8s60vX1e8gcY-$xWywyTzYt{ES!dn1F15nA;e#P=*ZBf_dhtoHl)#E9b!FN7xW|MIBq3{6Iy`%X z-?%(fsr+Ta7qQIF+?gMiQ5IZ;d<#mK@;_G_ z)0?pn9~F=fVbn+wQz{EkQ2yRqo_h9D91h%~o!ToN_Pk;BGE61@_D&Tp+@iD0LQD$h z7c=x&$tXuxmGA#nK3?6UHWXTy9%MwO%X!({myGzgGc569=K+mPNog*ndo#x_4d3g; zjj9_LkeL4Uy8P?4I4E9mgw^OB=yt8x%}8V+i=tZ?XD!!0I_P>{yp&$}EV1*WQ^hE~ zTgD{n)Kkd389AMS+Ggg^fI56Og@YS>Z@IbA4BIVVf8qDCYd56vx?N-PKp$z!$EJ7*JHLlZ|c8Wsj-j{oh~yVZ1T562LEQe+kOs2N-GODG}| z$PF9)M){ZoM_LT6ToIzc6cTls+DJQ_r%%Qr2=Lj)c&~*X>e>f^BD2WQ#&8MFNS}?O zxevzy{x)dq=sdeDtLd@bUb0Xbr~<4|lU|psHexKzFRheUCPA3Fo5?Ass9sv~vRrPR4iym*OwY%F_{J*23~MA99&@7n zEkNvN$tyWz?sM5up;BIeMv;VB0jSn6tngk9$q42~q~2%a)X6vOh1ROna7Z{133dX` z)6ii~*_U6S>?|IFzg2GDoe&`)!WO?SIPvgq_O9nWY9)Vk^jx%EsVtr@FvM1XE@Ty8 ziwF}tWX;cBBT;C@Y4NUIH9)sPYpy~hH}H5j0LP=d6D~hT`IgtOaZPGH_NnL!mYz(k zu&{S`Z0Y zpw!Sei3U*{Tdw2ORxP4TS1b>cl|ffo+F1L6q@bF&3;F|^3CGL5zT&W(GNResrr?zg zae%^uXR8CWC`*SZd;HHhdi*I%4yxG345Gw(oENz3L4z4rCR^-DVB?jUW=il8B9lou zi3g-{A0HLCchBiA*`aes_0JO?%4P+q~i> zXZG}wgM*JOif1=;*s$A#>mQyyFH*uW5taLA&F^Cl^s_22FtO3HOH_5PsdIxEl!|am z(pe{H*GM_X(B9G1dQiBy%@Np0x9URu+xwLtiZn!4Zyv|i117)b&gcY2xUx8vpR7uk#=z1LB)=Z~q`_Q=KIq&_R_H<1J-6ozw9n`Jt#5E@ z>fmA|TB#b{zWH3}Vc}}6nAcEfA~AS0T9fo?9;rI7sB`vnGb>uKww^^?p;xZ(pZYlY z9m18W%NA%mS>Je6SbS45jCvn3)Uhs)^ zQB6t&5VkW%9vw2m4iO0Kpy)WJA7#TU0mXexs;yDj zvkWk(8M(l2S)D5VCsEb*@VaVgG!I!3@spt>H=A`r&gMm&DwVSlA8h8@HTTT=30ho= z`^=)o=C)z=DNlCu&-c07m-3As^bYjCo4waxhZW!VlMC~;-Y#_oH&qE#@qhW^#3%e5 z=n#~6MwX-#4X6G)i%kNF^irdM*a9`i19r@@C^Bu|vJY77++I_5k?OW6lWUQ8|?Y_jy!Q6u&x2MFwd?!x~AYjPHfwEa| z%n{~LO%OFn9T4JABTb=9ooPcQBl2TV3DJtUO+}5xiyJG&7xK(5)W?g2w-+cT>$Vrn zSMR^LbLT8lFMsJ>R(`|bkDtMSSv`gTd=`tLFac!DoN@nva9pr#L+bFw%%7<3#MDkz zh^UGp^>`$xz%?FsPI;a3-g{amijtm8SSDx!vOh<=rT%s8)?)N{-8_55zxj^&&NWzu zVWbi(M)Ya?cxWhrVAG?9gX)CY%BwQXqkNS$Q19th?a;U9l@D$>bpf_g&|CU}`tQmX zf(&4rM*sj2;qreH)qg6Ri@mj-f${&yPQUhb6UX0T_xs^wmOxOpFa|jZA(^9>afJ?$_Rvj+^2wdmG;o)|Lg53-HVU0mNmHvo^`ql$SAj2ID&O z3eIhaeSqmC0O6vf#Eq8N44E3!rl^fsVV&)IuDyFc15+I700I#SBIDbO4uxzfAjFJv zIc_;(AFrda9)#yO^*bsTQQWiF&v<+nDBR#)FI7M_{@#NZjEUR^qop(0kY#NlK{&bH z9;4sXmyeu4t}SuNs#lU{pj^P>?g&D;UwurjUK?gz;%l*Xv)O%9v`F&ZL@$qrVzEBH z|IQSE{rL_G&z;#_795ye1Cj!f1P?e9=V1C>2KVP89RkFt0Cv$7bciwPT9c=ws;{iN zsn1;yP+_Z2gthzeQy|t5ij@VSH1N@d+^$~IZ#(g>xODHanU((#1HP5LR(gcs;nnME zqA~c4D=HA1F(BJ3o=N&`q|VoZU%hJdt5ZW2rw^n?@R;!VzS*koJu0hO?{@ur2TJjF z9dE?srB82Qo3Dkx1av-WNMs;YZ1Dx0^iQEXMp2$^3-}83U+7x6Aj?PlY{ac*xx}fn z>oI8aOwVXvUo4eduPSvWiQwpIRbqtN&Z}BB%niZyGSl@ojibmDX`Ld5n9+bkW1RiD zd!nGapzbud>DRZ1Q8Mjz75I0*u3rYEqZXXK*Qp^8%QPpw(oyvcncku4>Od3Gurk|z zaz%lJ>G<6EX>eDWFzlvv<^YrPatSO&zSXrY3^uGyXZ~v2DbihbghI6!k*%Cu^*{AQtZ?2!qH|z0rBPpTdutipL+-_A%D+2Uj zq+wznS;FfAQ1elfy={*?|F0?mz$ow3{*8LJhXyO{iAiFfaOXy*EF98!@z&9ok6T~eg zz1%H1gB)dJhR4D~zW0LyzLwp7{p@Su;cf)yw=w#V!ynnb=yoJ`iv#=J=&Ziz?Qv%4 zQYE(rf$ZG2Ipc?0!2&AUmIcx`MjLn9OTpKewY6>qDNkU#%^FhM3n7sBLxBs9etS$V z1N7)D18&z+0mMfJJlm^_V)=}0<2a#vd|xZC%N*73lxAVR!kGrmRuz|vF?7#{oHs1Y z*<{*chro^r*s6igyX_cPYUom4xq~4y=L1QdMD_DrLM=G!F9VM?|sCFDueG@0(ltdbh!$oj|h} zyR`LPPA8`O&=}x<2C81)p#tBYjAKe}+(Bc{+4QnNWMQO4WLZT$iKM^wlbDfmz8&t^ z0)cMq)G=RUsB3;ig-?NPJ_vS> zv=G^y(Q3Fmv*Xr0G4z))w?pYX7!502g&PUzEwpZ7|Im+Np3ESb4Cl3&0|YfVEAtI5 z6%9Rk0z@L{B9y)fXsX~!gu&E7H;{+8_H^foQty7e93?aIts;i4xS`Vsy65-Ps{f zA1{S45Uuh*YxWm3a{ z*g4@|gp4ybjrz8VNAf|?%x17F7Hz>{kda)I;iMDIp+JrMs8e}|lKhEe1FRtkVrx9G zN5DykPU!T8*(U02VOe3qN?Tfy3bFZ-l2a8;2t46oWZ*0S0t3`h6sS zXoz8u%lCl-Bt%yV0|^15O0$x8gXJMi|X3D$=J`aU4T(-LQrk`gQ#it`VCs z^2VnvJ+muq({VtdnzkV8zf7@vE4j4=m5>gx_lik415g1vv?-E9+(sO|ejVC@i0dNw z{}0JPHov4S<4dR@G4m%PO`$^c#4?kW>N8dH>LOeAnaVLBNPLuYt>K^8NnS3v62Acx zTCsV>i#p>~pDh7DUw}gG*P?7ND_)RPSqLgnr7RNC60J}mSR}6%puzE|x~$ zR*=CZFEmq7C|NC8T!Uhy4h>}OrsJwE;)Pumv9%D2=1h4;(r(};gwU9U$a5gBoo7O8 zsQN6bTeX5bR9Iolmb7~e!xy}woEgdVxh$@0#T--)6 zjE3kNr6a4Q27(naa4AK?QeJ4TmR3wr(K3ql9SJu1a7r4u1CJGM_*7<+EO;W67DcQX3LrFsZJ4=F?^`i8dc1@R zr#1_bTwtzcoBkrSoMJ9X5n51#t_2K40^A1RJKAz>NJq_nG_HdEfP#o?Ln*niK4@0_ zKr+0x{9rKHvZ8(3PHX?36_gocUUv*V@vl7@cAo4L2_%!nL2oT=VBl*`feECdXpe~t zuHKMK_Lf%)D(h?C;@M8OdA8$#N9kxBIsv#-CxB7xM2Y7+lVCD}iSO}Iuz60m>)p|x zl^qz*8l9YMb8v@Wj@BaS&f*2+{NxkQ%Z!%pMmJ}@XS{5pMYG;>flkId$mJy{_8PQe zZ_N^*`C(75-j|KhAXKYXLOK67CZ1b8|iQt7|dt_|rp=#rPvmSC?7xI`_kk+U^ z;#w=Fc`-+aqyWvxbwGKvUJLC5{W3iEFopB9({{Mm%#J5LMyvOks*8+V#Rzz=TJPtI zlYUMJ4&+Z-pwD7a02;cuaY8%mkUW?b-c};#)zT*Mx;hheW5v7?yOLGX-Tay6x>n}% zlss*j)UhFE6NA1jnR?UNEsXtX?D|eRoQ|Eq8@UeWe(a1!ZZe5Hf0#~TGYlOsAgME8 z=arbxukrduh(p=Y>w~+j?Ty?op53aNxVb&xu40Qn+rZ)~D47b#Xm7bFY7MG6`ruU)z#BUO_rM8Lf9#BSI2wj-IPoT%(IO-C{@2$OQ}=$F z!ba*>Qg&Q087YA>$toFNSmzmiz{WG>joX4R$Z57LyG5*?84prpB9Cbvp}eR%(it!# zVilwr5KF6emX6RoBiF3;KofwH%_~tP^d~1lqNpyc9Mjc2#`Kt(PCn@ha$6{tqcImn zVLek(VBBS}0i)kwODb^c@(626pwC1lP*0%ukl;)uue69WPzJ~B7gFFkDm5p% zSmV(LpPMa^rbN`?@v_-Q18LtBfV4+ZLnV2Viv=FjJ+6#(Kt=bL1=Y2?_ttoS23PU zT@EmIonaV-PJk6-;V?j>AB@AuP5r>#jAYs3l4qGHzd~)v?OJdnCC5uKxZ;5(^XX}_gNWW7e^ zPp8jM-mvo*C$CP9cBn}?HQp=xZ|tWR$LH+D@!Pj2ug}=&Yn+PFq-Hu(cha$!_M?+` zC##|C^o*UocyqoB1RZT<9p3-nRV>4QB+WK5GJG0_?kI64Nf=?q9Gq|*4V`J=dXscA z4MxGHaJCtlJBa74xtQDKvdzVK{_e?`H1>li37l{exK1!04xPvwaVH#yX*!G(9;chh z7)%8|T;EK-ysyn9;7tmrtRNA_a6ehE4x{AXlL}OwZ3~Y7o#z0@UXX;GiVMA|6T~ie zrqRfE67a(y_J=+nZ`K+GG_O zaF>xGNfWne4POOH)>TaOxm?K?r9!J#ftbF6TNHu$#`4J<)EfToS=7zXcS)vh7mRo~ zbiD3`#zSZ9$I-}-$1u6psBjJ$%qyO=*ND7Eh2AP0+J3T4I>zporK3rt zV&;Srbr`lZJR=lFK9c1h7&|;XM=?4&JiH$_GBzuIqJngx*#as!VzZ(#MORl>DC4@0 zuoy6Za)kr`>GEl_(uT6wn7<>L@q+79!u_A$ynpxc*^A@zkEhRDrzuGYH9Y$PqY7OC zF7YV-F}JmZFV0`Ryko|`!T#>t*7@?`S$iHwNi-U{jz0;x6QnM8_%QaJXqb91^}=BS zo2+)Ng2MYsB@|vrF^gNd?mu)1#l#KcVC?#ipNwKBn5Lc+20V0zSZNdvWANi^1z|a+ zv+pkSa~yx8#CxuL8}jk6aV|-(DyaSlCTfI|w>l%ON*n&#dSOE=e080$rF;W0F8kV| z{(F%rOnadbbc(QG$16*{enzXQrwPBg4JTi;QbaprWOn4y&X+rIslWZK5{|tvex`y8hJWHR2Y>GlS!aZuQK1x2zX2j!vlK1N~Ot=^|yUz(&{rTa`1 zD1DoR#C!yKd6q5-aO`6;PKZehW9A~3MGPf&gs}`LOvMWDJ{K3~@`84} z-dbtPgMO8ftqzJPF|B2lW1vo8R;ACBK#8~c&{dYZgEosEn$DzEWsxnLzPo1s7>;aa zWtk`_T52$c1Z&aAlZ0B_sL#xxxX(-(fo>{i2-OA2-QaJX8uXJe;gckE;t6-1AdJRN zG@VYJ(KHB!^JWa7hSn#U4dDbS53 zd>G~Z(fFQ}M?=38<^3aOqWvqt**DfgQ`{Rv!KSDQF)b9UsYI+(VAz(4nVN~t`Bf27 zi$1#ohNnu>5I;51C1jEPrrrC1w2o={X2)+&NlTfNDSc)24~E2ye(Lr{qMAf3Wf^3< z(%bjE*bhC|8HMrG3H)H}MBJY^qp=qcVKm{BYx#deZ{y1#)$=G@K7iULEpRVtk9VZ@ za1(4p+BM`>m=zKlMVo0k2fcu5SER<1F^v4-*oh_{#&O+|6Aj(i38%@3!zfAIYY|U_ z)aRJb7g7B{0SesUzN&Lqlm_cSvv1f-IY+nApf)9Rx6YsmSwPoEHRWSJN{QBzJYq8t z9p=Os-=97|c>qbiK#vCh9)|8V3E&4?DDdP7nUCXB_ETe>zF)GA-;M`_S%HMY^2bw6 z<5U{}Yi*iP)~+lm)K$nFYr^eHEafp)$r;zoC?unSD6XXzpf-IaTd=+5atm3Tp8iFh zqb{XH5IT3V7_d_;UFEFAI~vkDWA^+-Szh#$O_6k_1wO>Gai}gu462pUHJxnhm~%)3 zAGBqum5kA;t?GhQW{N&xj&2!N@PH{h^9Fc)>v%ADh&yIYjR(`l)Uyi_Utq1HDPU`m zxzz)Rx_-s&KEUz12Y@L921O-e#54H;D-WIgf!6r^^Vyr%n~p)Bi6YMG#N?34dA7va z<}h?ESHELq%1WlDpmJ9D20I(39#M4O!9vI9SvQVOw6t~jwvZp`Zgz?G(WJ${Uut-c zHnF`61q~wGzZ86BJP*ELAMx4Bny&FiEGt_AeEpIE+~bh;xNaw!9d>MlZ6Qcn&updX z2Gmgz&^+6fV>91|dQ5C;1r=XcRU)00m=_d6ygaQ0P!stJ^d%Lz1+?S-PhN&*FqdG7zBI`;zR$O794Au6c6F z341Nikb=REI#Hoc!R`Q_u}-YOU=>eZS0c+;0R}gg2|5EQ9lBQC-)ajI@k54$F_Atb z#1D3OBe2K@W=z*t>yB(0(@>7$cI`)o?V90+zd9{1<~-i%LNO)5=37fNie@x3MA8zh zOHs9U$|WOYPNi$Ofr3-P|Fw@%Cb|>QqIvsoN)v59z|T%oT#H>{xjE4LHBK9>QJiN=Zjzm^kVacOJj9oooVTZO zgDC@-y~zXhtcDz;2dE0M44(m4SXSR8gGfqZnbmn=_~{ps{L;sNf#XlBK=Id=(*0;e z1|MFpM8>DxqGSAD5N%{e(j8v`I_XjfA$18(vOBvcz3!?WO zS~}#pj{WMkMyuI_CxUz0d_eE6QI<=M3#7+c1*v6`gTsJ;I^5L>+Q`+G-Uv zNCnlaQKUy&V+Ab7z*Dj;crId7XbKtHsipwXP_bSA6?JpHl1VFO&lc1jv2q09lXkRp z@MpjMi)7B=xOqeuZW!rRMrk&optg&IjC!*)6|5!BODRisMv*LB{-vB7!zBuV`p3k8 zNNca@U|iWKVKe+t@f=WJAFgz=VN=w`_LygNut6!A@um%>W9u`y(jaFg23(a{5$W;l z)fqzhiWf*Tl5EtICfG+LFGUFyrlJD7#zR>HzMcLZ@XWy`{k5>X!WA$pk5z1mAl}LV z-9A~Y41}zVeL%RBM7C76AD}gWnYe_O>EBk;IY)c@4SBFyvKO50E5q~gN)k|Qy~8D6 zid<;bQ7ctlta)eb5~?LjscpN^)LSHTGabvRJ`=TqtW1YZlwm>WQq}`@E*Yoh5Ol%% zG>VC%R@EII5^#ryC*+co_A~C^+l&{7hm9RRm*kwyZNn6L+}N#Hm=+j3J>t5=^lkZp z4L>g#B*L)NFKfZ_mZ&zUZfUBZLrCNoI$uFn(n=^ivS14oo2KLyxxLR4S$vDhB_&Mg zUbeNgIpEc6hlkPXB>6{5vEX^~@NnmNq7R-s@}^EQ^uP&R&v(KkPMrz(5c)hCj^fP% zBY(iQBKW}_)c)NwDo@Y3?RQ%_k=+xUiSrkU4boz!?zRZ6sU{(Iw=S+dv6*oHfHm#c z_SlPEoXCynd5A)l!a9#gOrr|ThxAQT(9WnWpgrzV9;gGYK=1eGpJzo|zhX7M zAMmpm{L-2TV~EV=AlX>LHUN|07&x#ncAsKOPl(+Q*uJ^z@W58o@0%X6gIN#V_>*Rr zxIbF2v&o~jGZfFwk-S>(vF%pqw_T#FtShuxO7gN9fsL&=qyUN4*Vs>|GPGRgz3uiL z?9%+cOJ?io;UV_FA0Bo(;3>&vj&d78WUt$u*9oW)<=U=&j?w4JDJzM}17|2jY?E7- zm`$^v%_Bi#tSm7bHc~FxekU_!!p^quyopd_CrZPB@QGuEyHTo#hqL0{`c4+#FDkoNLsg2zve(16@HtST` z(uTb(Rer*H3!{mNzUbCq=Ptz`?G*%~5IJ_fX?C>`8de<}@wN7!gIV!kVQHIykrXWA z@r7v+wy`l~r;RG|K&chQ^q-r?A=@c^Fe}a|bSG1rXzE^w5ZhLoZhKz$K7l6^5g`eSG8wVpqSS?r?G(41O9k+W?peF{9k0E^4A!W)eDZF_fE%QGB3PE^pqS zzkPqset7fl)$w_ws2n<*tQ6P*fqqNV=eY@0qhS4k{rkWC>%X!+t6)e~?5%W)b-}2N zvw4UA$|T8_9o5q`bf(L))8*N+|DF~XT3)cdL@e1}T=7(IZ|XcVs;%v@@i=h8u^TyV!h^|hnhX=z%*_o4c*tk3;mT-{rgZBI)o65nHUYOA zlias^qc#B5aWk6wzqV=}?QUBd-|_133g^?c3T>Q^uj;C|jBm_T_mdUL@T=1TPvvuT zomNA-QLUS%Zm#5AeRVxTcGI`(jY~~gep3dP^Aa6`SGG#ku*~=9ysUH9mY$5YYHHGA zO?6q10w}f^?k?q+R9SoW;^gG~Bx!P5jYbpC>jlu2_J9P6Wvlb`5@sl zJtwzcKX(DtHz?X#z7Bz&KB!gV4r0A7c9@Y_Nlqt1Ot7rpbmPhE(zMnVb8iV+Yd*lO z|3h5;y^5ooF)RP86lC?*kuAC!6&OmLE;Lne6kQ4X?15*Dp8R zQDUfu`tMj`Hl^b0l}!IbT>c)#KvOZr!V z+c=q^8 zNV*SKZ~osU4+G_zYTXS_TmKue2Xgi11zbBpbZbY}!vMRs+51?Fug2fk@B8jp*XFv~ z=EYa^0@kPg>-a%;Tw^Pdu3e@6aWAS2mp|Tr(Ba2%N{ck001*a002e+003ikb98TQWpXWZb#QQRa&#?sXkm17 zVQ_FQVsCUUcWHEJEoX0UXKZCHWN%}0Eo*LXcWh;3XJsv7VRJ1sIW{&fYIARH?L1v` z+(vTG;;-mzR3<4Ay8sr8#nNGPBxO6aEs2rD^Qj1)JeX-&+E}^Q;$)2a+ioG17-J5y$TE>D_+~_u+|DwHgn~(;M(~9Y!xZ7Mpc0O`n54*t z2oaG8gfsX8#|ckhd#{8m7|PZr9Io8EcfNA(UcI;N#(m#G*9yASv%DaPtbr4IR_qaU zoXF`>+tvW?%~0FkgY6-5Ree@xnfd%P9`FtixMjAoQo@IHL=xU6jmcwj`Z7-pCcW}Y zCS;-#!8LAyOScE}$96MR(xo@o71Bsaiw$Wdm3OEl^ewxai<5w>7M;}N#DDHS=> zClL1+Gk2w{leZ*)#q*Nz&FP#3YqCQ{JCr zE=vV;+Wqn~Nhjp2g87&zQb8;SCnQljs3wTK)@5F#=}NU*)4F8k007nMCM5geWEa7}A8}a%#em zpLaJd45TpRVaQk4+bU1SYg35clv~=u3O*rRApG>+W`3vfMaq}|md;Y>_`j*uRIFH>*=|GnX4&J?s?Ot|Zf z9>G%jx??fMS`Y`Hcx!EH=+qYT33uMH?9=6kIETK+0k=Z1>o z`!wh>>{*`EC6?#eu@%@JT8>tmeuPobUv!;&@%3tw#LN7p9lTdb8d*dgj#oXdj zwsEg@GNW>%{+42_eooTD;MkgerbOZk-Q|z?)kp!v!2)YT31_zs_@sKMwq7J^+e<;`wsc*gBNvbmW>3*($STh`kv!? zLBBh&dqF?w*7I_tNtPg2`?Cd83196XwNoy=BNbc`s*HD>kk<**T5zRYo#nBrPtYc4 z(QenZJiBXK0qys#0Ey?(o=*oPSSHnJMkL`w61VHb)pO^Pw>fbIjQEI*SX~FH<#_?I z21Z=0$0FK`9XlReE3Q+uBiPxz-IRWNW-;>?Dsk#SE=zLj3Q-bP`Wcdbo?y!6+~!>n zGaax&*P&gD1p&1@=Jc#6h|%f?{jS@mwrksDLCn=ryE;|d#Iilh>08=W9w&TwL&o`C zo6w%iEQk3$%d-QtBHz&v$)F!GMj~hNu3)J^Kg>K{=E-n4wWTJby7Lix&Pe>o_j*y^ zwtR<0md8A1k=W~7ZrmNXgC32Yfq#vWEa7jF>GP=xnyoqoYaKN>M{{KQL`%{Jpam+n zv!szHRC+jFXC<>RJ>%RW0N9-_XuT(FEq8CR=SMv^uxJ!9%NtOgiU(btTe%q9G$3>_ zXzk~i>AR8gcIA4xk>q7gxQsDqdGOtQ5# z?Gx5v>u+J|9P}xsN_;i*NS0NR)c{xvmuq9#S`Rru3EKSa8u0Z7=E`CVI8IDfy*Arg z(@&H__PCt#?fr8-xjjVnL~u1)TZf0>T)^ye3E*O*@#*s-O4&b@zL;a&YckeIJZ_*H>YYsaRUqIarjtqvaNbK6~fI=j?M zxKw#TYfEi?lP3y69|A#%&GIVl3w>pw@iVu12E!?C`d-b*0v_wjHO=UEZ+~B|`|MQZ zgesGP%`R9CkSrw$2hA6V&OkF)--jhi8kW{VFcY2^TwaDZy~3?kFwVsomNLCuyi+78 z0|K#*qjT)QQ$kgG21IUZmYqZ#RmgWmb{6u_SfWCHNjQi4Z%2CeArsV~X?|T22S3yp z9F-_MFNM+ETk!O(UCTmq{R`weXF&G3$SY6r|5EvBq;j)};0zdgKBE?4EU-LEm=!o= zVENdKx}Fm|ad+{IU2*@#2vN!F148Me=4Q^NJO2AG`>9Ba@!S#nq&mn=57-Cth8bj2 zvPRU6(_*XfF7UkoPE}htR2y#cmyi#mf;U71_WBv^4?3`RnQQs@@VPk+LGE2$X!i|K^<@H@9o4Zi227Xl}`LdA;RGqCZLxW*V zic~!zijFP=poH(>fkgIF)H53Q1k-DRMS6{rd=#l4NYTPZKMFCmj@|}6m-qTnNSYeR zZP2u2vmb@1UE{o<_tV{ad0oxD(U{w?ysWPEb3zC2H%N zo1d86IW&G$dY!vLuX9`Ux_5(K_qOQm-VJ)Yw?(gaH|X_li{87DRyntoS2=ehv2t!J zvvTf6YUSKkZspvK6LR^`IUP&@+yeJu2OSsr(UzIA})lWmOU+3w(WK21X29hRHP3f$kkXplpZB650ixN6Q z_8euAssjV5RhN)6L1p!v)uv+Hk-7q#860GXb)17J`1`%l7yHk~&KKTUN}hN@@?={S z()snt&a>{T$Eno~w!eB@d_L(QOVnRqy>qP(=Gn}dZ-J;QxlN_Z_on(L*!~pEc~r=3 zio|cmV18hEab#Q3px?8i0ri>Z z^liM<(Dgzk^!N;(3iTeMq?y>5*Cfl{fa@q4p{~Q#FVUL(mD)?_$9cWeg9k6hq}k@C zA4sRYR|mQ@xY_Xb;DLT5|H~hK`_EL+vwx^9GU{XkzBcnMdS zbCS+lB&Kb2ZFnj`_g9%}0*=T`fx^GQ8{K|YmW0*a73~U@e}v~+@qs{^oHXkc^yqK? z2cDOY$MRCefz%WJ{72Z$Nvx!%yli%=(41?NdN_THeyH~j`MZDp4>*u0;aP@!d#_W~ zC@KNNsw@M@wDZg0`6tj-s}gj}$QeYsqfO{eu^O$CoIn4su2wejOD4*&)|z3yqfyiS zTDz$z7;NwF0ijeBTe1=w$25H#;K! z>P!A=@1X1%{L3Hy?T_$6@3k3nrw#i=DlBnt5VZFglCnVCRHkX`2zl9+#E~`>c*dDE z-7&Hr@@^ZRkdlyTt%(khA@^qM7g-YWUK{p{C{5(Z^mQt%Iy-7bt6*soUWww}+u!9{ zEjj*`Ch1SVho^`P{tnYj6K7f$_Y@ptk3zl|Yw}hZq`DVtq^Hml4L!?Hep%w%`+FN& zBxX*L&9$KtN>NJFGe}JXYx3(-K8aE5`DV&_tU zfG8(icOh;VPSjhNTmdBJCkZu*xU%@Vy!h5fVbLXF@PBjK{RtjH0D zMZyTDhPB&!V2W!shvH;dlo<-FRr2ao;#${pcPcdDliHG%;5d)3p7*sF4h=OqR^6l} zVJIm{OZcf6CREQgeV@7>8^~09P7-1YjXoQbB;5oxBKd@;|sy5q619FOqSV z>V6RI+dj0my!3=Sg|?Hj|7BUu;kfUuEte=YJzwFhMc{i^r06 z-CZ%soJO@(`=eP~m2UH@aBhuB!lz|URk72wGK|;N#ETuZO|}{c)2|6e`1MqIg+$$lIXO>j5DV@W~b$6?YG_l+>Qu9xTIP@1Rl zMo3-V4Eu2?&AXZ!C6c@z*I%y3YyKaBXyP)1aU+BlS`BW*q^ptn9_q6sRNV-fWjzxg zhtg6VxDifuXY9w}vAmh*X1@5UMBoQ8u*>~f4aL?GvmisP2R=f-=RtiarYi>7R+TzW z!x^3;Ep#k+mb0>y`9$CQn7LWPtABpt{9@%|<^KRsO9KQH000OG0FZ>ITrBtXYd#nN z0K8oQ06hQz0AyilYMmNWq4y{Y%OnfbZl>L zYb{}JWG!K7Ei*JUHZE#&Z*I+8>v!8Wmj9gm6{y;?wtEbP50C`2NoL%nnPewvvWaIp zbLyTJj|+(tiqP<3SDTaneGfoVq9{t1EVsM;&{%u`7Z<<##J&9d@WCjNqsM4;qp(j% zI68m$V3e^Lm}@Usk}gmNH$@6%2{|etzfJ_8sz|a4`u;Zo={b5=m8sUB5X&UGcwG;m zB^0B_=%;K7Q9k75e4N&#Kw z$z%d~bGuy3?d^~b*F{n$Ix`eu*CEb1!U1u9Ztk+2&7g$bP;9Av zFv_bGmcJg1lB|UMnx)N>%zw=-LrOi@Mfpls`RFkUn>%0Xe}8_s{PAT)``;liYKq*k z_prIuWt9qj>G*-b0gE^$LJ>?O-@$=mkyI*FG2uN{{RK+&vEqfu6K+q|*Cp|Zh+Y9yL9*lA*bd?J`=Z(%k6;pt6)Z8=|xYnOKy#3{u-0-!`D>!d1 z&9tGF;q*y@@3qauHC^cqoC)@w>HUHN0T>phtLGE&lr_}0})b8nB!swp@TVa zm$=|C;yVLq`dgO#y8`r_l`zqH0=HnQXg7KtNW?Kfa>E0G#EK%BWHZRhqQl1ZtDDl; zNDsI^bv=p$M+TULvBFe196L-=K_g$eF>SfuS+nRZ^g(yAEYI1ZbA5%8&!5pZNj9&_ zF-z0av$0B9`IgO3lM-g==`&>h9sjG)*?2X<*|@UIJv~ETzKm{D8Sop3s)To$m8&Iq zdzx(f(1JKJ9JnkGfLDY27ziHXLuBuo0LVg<3= z#97+~94^4i_YM%*Ee3##SVTcYvCl(=NuW56RqW%Kgnkr7M7mzk+qH|yB!>wrVYisF z-ecVja$V}2qeFH6YD5FK_Qyq!X)$sW}y>ePNeI& zD)v>}_YoD#K$5R7KVpTcS`R^P$mA@VSxL}12fhy&{J$mihFAl|-=-&+>!LiQC129X~Gu8XON1ST95 z<{=YUDDWlWG~#5(j+m+bkma-!?FBL^TouF$Ga3*~e9yx)kP3?+j;V^MCxskXQscBO z?y{DbI!E6#F>QV5#oAKZk@s-{_j2{9U@n;0I(A};2~ZEmZp<-fF=Y_RD2Pbka4uca zCA)92p7S&*-&KRIzULj}>NnO>LQy7}4dbl&{-MwKH9t<7M=tfpmW*}3{V+!Zcqk*n zDON5>Oc?ht6@kJLgd+4qmpeh<$=cY_J-GEc`;L`jx{RN?Jo67@*$zky0wN@iQcU1j zst^-5AeajoV5wN_2%i#P_5raees{)0;2h#o-GYWz?r%jy`{1EqAS4JG@i~^t3o+rG zVitN5^H@0nC`CvVZ{ZdCSj}cEPyS~kLA5nfvj2aR&IZ^PhPSfKxC&OyeEl_g0Vd^R z`FNH6`s=S(+0wOG>PoqKW}L%3Wx}}Urzc;ZoTHNy6NB1U+sw%fih@m`yDiP7zZ(TN z?9?_-ShD`9D>P--fN~I!T!Tamb!b3ZA%-%Vp%P~El$C%~u4ky60@6w)B4O$3)`X=g z=5Tvf>j;EINyp>-NR&_(@i3q%%M|N9B!11!N^;0*W!ZoI>;B0u@tTr4prD z7mzNH(y7@@GgwjPl_;y+(DV2z`xYfOoKp*XccNKp)?of_$utkuB=DM^8I)5k4FzmI z&owi-NtDdyDVV?jq~? zHb)olUl*vH=DM0p(IuoX$=U2`G;Ew2tju#go0pdj-X*9cgB_AL4u)heP=p1s=VB2X z(;Rp{rm+{`*ddN9sWfp>PxjtIKDi~*opm1B6fm+=!rCl@m>O_=m$OVvw=c^d&bz5N zP~4*e(6`Fp8g9~HoIF>STRKzLk0CjYyFe6g0p z8U5Q_dcR3QkE~4{v%@Frx?bOGb+0yk!*+F3F z*NLu*?=6SyyUlwNWHqmS^P%L!>Qs0w{a`pSF7?NZeS+t#sL!H)*$5tfS>?Wl&m4y~ z!jByyJ?7z<8Ic2%NTNPq?g6DDWR$k)qs?II=(w}7G6%E&9)n3q+)x4-%jy_I8^xG; zfx;nYAl;AzKG2OLj2TZd*jMH3JjRK=yJ?<#KFIN-IFizHvF{RsNdy$feh_0yTpmYG z3!a{?UI=35>txg`-DBfli97-IYdkhl> zp(!S@VF<-SkeCn`d=Z2l1z&3+JJ)igt>&Pov{J|><@7IQ_kr95qnGPx`S$LY)wF92 zTpf=X%VG4->OnhR`=oQ2hu{tS#g34n06)OOp*|*(hnNc*;aGYuL=N*g>zCX7Xu{{e z6_BG3pddHdg}e9&p8Ll;2{Mdh&jpTo;8ILNmtdwsA43?iz;gloXy~x(t_l5p?9e{> zd&dO`!C^R-j*p3_Ld+xyaY%VA6%E}m8uD#m#**~+pxX|-58;6q`VhDtcDZK`9t6d) zEg)b)T(S zA*6JU3}-WaGgdmEvGVlf#S5D{ee!7PJ~LU@Z9!8V@~p*7OVzetKs2uPDtd;TCN@H7 zJgWn-=E-O1*Lv(HQ}5X~pW7S8!E2JAwU1}W^%^psRB21l=M_oyl@;O3jN#K~Xmz~X zBhmgiTfS;&>%RAEd*?}-JZt|(=m|s99MsdRQ7`AT$}xTOc{yjtBmHtU>I`ER?LQlO z(g4Y<*zZpIh;@I>*9PK?wPCl>XA?U*Un8$~_Sq-VFuDJ1{akm#HQEsRk69w6F8{N8 ztw|~&ouO|&zhEU?CNmhD1p4cb?@l*)usZKQ8<+ZJ2fSapOqs7UWr&+5iS?!XdD3j~ayiT0^$;#4;E>q$-p z31L5yf6wv)(1$yQ@Ho6%8SGK!nZ{BHC&IpP6())c9J9c~ene;#M~u)mYkUvp`5+4{ zLeCQ*81^8Pm~h51=MKf5AE?M7B7(s(jXPT4!;EaK1f$^l*!LZV2@r`n30>?3u8H0b%Jv});*{h~=FO#VP#bu;C90p)AS2DsB4BjcDERMnVy?y}o5i*JD z()}07WOW#DTOoK;$G<(ATv^T=;8U$Xy>4m_B=uDsVHb>5_o!oHeu=$Mx&TffNU#i1 z2V|$H_`g8Zhnag@+&1sQwOl;c#=4cgyrnVZR^xO}JB{A4=bamO5bE40^JI2f>!)U( zQhxxE%#%iNTOVsPRvq>aTb&^5P>rQaH>y<(MtR$TGmQ;h3-qM@>(B5TGlzy_p=ZyQ zai6_)sSkRm!>u+Nv;NKoG9WYWHj(cpL^2fMVtI025o_AN&-EIJ ztVa;-^azAbZT=)`)_s%6a%zs{h_gyHwQB3@uaP@Oz4o2DAi_3+TxCDK0hVWIrgK1y z>#DQ`2KJ^c68QPFbx)-h#klcNP219>PR$gLc$K7Ol09PcdA-(0+YAAiVb4slK|O(B z8RB572!Is}bi)?5_M)`Jpu$!(AXAKQDk6%`3T~zv-6Uz+7J(pBz9Fq;lGZYf(T`w< zXKA5PF$ZA^EOeIERTiBPwVc8Nu^f#6uPx_|=9niSPpr&quAv=T5ZJ-Ig7Bo{ZsV}1dyJ+oHCki1JkR^ z%lD`$A2<|KLgz3HDl|_>bH|8|B)JR#jSuS;Om$&D| zrktqCyYL>POKpzB3;p;ibI%Nv2L@~?EZ6s;?)~Xb#AZ616eTii?@V%)d1vT_S!`0& zBO3ncOn{lfjF_XmO+Q+XsW-rZ65;?j+!G8t9*Hm^k&9WV0vvmek`%&7#o=}d@s5xX zdsgc0G^5+(BcL1d;nHFG@Qt2rQc4%Eo3gzHS|-abm&IH^eE=r=Lh>Ce5`o^ld3&&g zq*YNu-Uvyqb-5ugz0$xmomq+baS9nS!$`aHRgP-6veuvm$0riy%ox-4s5~jwisuZBfE9`9_!TV6>KZa>%FPTBORZfMmWicCOb9)j z8yT#TYM!>mS12j#Ip}xOPyDfZ^$u7RXG&Xbb&j{ME?vqV-}-9i5A<~N*RV|W}}f%f+j=nXaxI8)o${9LEiEGw=ujBEz^+f&n*aQ3aaPg;(+y;29nI1d z3<#*R#TH!V3-sE0Dqme?21>Pzb{j&h&s&%_k<|xiPOnDY{K3`e3>olTgX!gHK2B3N zQp~F~ExP`1QD+g_fUF-|CPpXN4)P96TW4cUc^6}2BVHQ|(0*>lD(Sq+YCXb`WVJtF z^maXKXALsRL6m8oL|K`H>x7xLCZcXcG3jpOFj+Agqt^;8bairVRG36%t}C8`bz+R| zspcyuyIG%^eqzu4)}l`i$t3H<%a)txwbQ;lt~EB#M$8LcA%;?IGdrfw{Y?XlVr{+MZCm4H)@*VWCZA!tc+iY} z0=oiZ@GTkLjD<~aEx{@+AnNFc5AU((>7Gpj#5!2B{Rq{_G=X@)^M^@Z}Z?A z&)sfF^C^X89&SZq&o@uJ3eGZRlCsTpyO>?wx+shCD}UN)@Wai3iA&K zrE|C%S%&FZV&r68gK*`5^vb;YjKW;ip$dp)nL@>-a!?u0_M zHt@|(8vFd%TIX}LA#>)?vh#Hx!!*{n$xHF=NI|_ZGu8^yn%-_isdj7GHA_;9$gYUy zCU0<#K7x2RFV4|NmYR&*>q}&R*6nCXHaSP%XOkoY$nB38aBc5@*Cs>0OSSk^m>cHQ zAtVN z02|8Lug@iv>~c~zT3gvMt>36JYlPRA(TXjS8)(s}G3#2$A;tN&zgcmDrS(u~$JL|| zBb}Z*S(V@3BRr4r3%NVM`Kz?@ve%ioh_725I^St6yy_h08245oL5G}_`)%~ScR=qO zXh-C3>WMYOcIx;sm9y5gG8HbNf`$^q#@f|YOa9}=|CTALHqQT4b&GY&Cwi74qn<TGl4{LJN(kZbOMKUQ{leL7uF4wHhVoK zFl_DCq4pDsbBEErl;OT@>9Oa`X%5hbf9EM%mGnE>!KsJ(6TZA@^=M1GL;(!9*#XX?DXIthtRuY?`L%DSI}VxuDVx+ zPS+!v*Ws)lWozK8Xe~g)RVRWDG@B?-*!r|>S>Tg}mD+{_H}2^Shx}&iiqj5#h-)h? z!Duj$H-3rXlm9Fp>P}y&H$2KetzI7cB6!@JF?b?-EvMC=I!&$c#Q_#GVd4jnD;B#E zXGc))`bNlkic7;CtDcS(#J_g4;hYIYYQ7588dYR1(?B4ykvhA%rNN0S>7<-`KU|fy zaZ|5jP!{zx+sD^@69r2mgKDV4#+y!@`Z4e#4x&f+|M{XB>v_EQKYY3nC4QoL9Zi1v{+B~a%gn;g z_CI{lVXbRh90|v78Xl2A9BbE{+$4y$R)w+2O;c^ysiz#*WNLe_YUID}#uQ^%g?2W~(x zgM^7lN`Vs6&eN0Ph{5E{0+b}1P*5sG%RD&$vAG(4Ti4`0E8Bn(^`FhEJMoBNyW@d~ zg>>jh>!^_m+=le()2~iC`yGFh#rU9J54x_rTx(W;hS6g`<@}>%w+dxU$0zmXHkDun zq(oTJfrTwtlF<&B%Izi+Fz6)J~f-Doj1{$P-4`$)X z-XvM1^u0~3FJm)?KuHg(OtNBew8cb4O};6U^gGZM;B^mb2uqb%MLXdZ2cz#Ly7akY zc7}qW1HEgrV6kDw)y*p904&p3S>t6DQ(Sb{zU8KTRUG&XBS^SsoR`>{i7EsSonnwe zrtPXQf=n@Z4Z<6kw_M$LE-LVT6$DOBGrU}a;qpV}}zvmq+QIoX1fZj#20 zUP@no*)_koIWX%a_%tMz2re`YU^gnkV`qr42(@BW>0P{w3P<33asIw7G}D6UoQ-+n z>-oOHczu#f&k--I2>cu@>}npV;8jxGOmqep;`4T(LFBZxM=E{86z$)cOE?w-)6890 z4btu6_XT^n+SHy~O1|Ql@%o3=; zyY&2Xe(gG$ROZ|>)II~q4rNI7d|R}CM{klhsfw?jJ$V-T^j`zhZCWBa%&h zWgZIDwcAa=h8kp!!0+h{;&`TLt{P)`!Z=XvS6EJ$eI@d3->g3IR>C&OodL6Qr zJpTpRu&t{Y5iUo_`Yd$GwH@~svqPW|8YAeo%P@Zh+9W#fH|Y>?Fn)>G+LPBC9fgkt z|E}wM8053+)`jna$7f$3Sif=QX&WowDx0Yx%(Z*yfZSb}<-}dlFe9>(EL*qobe^>v z=5Ek?0RexO7=I9)-G}88e$uuV2LJx4%SICUVon{Co)>(v;0Pb1a}73XY8<|SzM1c< zlt#iRS!xK?nB-fVylPwwGqJSUrj|Bi8A$8RKZ}BOTAf&z7|Z#z{9) zd1hI5m(9Kl%Hz}IM%_40+m$~SBy-C`SO|Vm&8#({g1h&st^*btErYr*UQZgKUCWZF zfWFT>k5EUJXyO?#%=4tq;7M|iq;lAPj+>hK>Qq%>+mdUl0pt4INMn1H#kYq%Nql!t zTW>p)YaIT(O;+%^JG?aglt^RsSLPEpQj6yVLDLZ3(TRX_*kNk;#lOf@aYB%8l?Q!1 zQj|kD5ennripYftX9tA@Ku33&Y=?R>DGvme<89gVTi zuK2U_JGOv{N3Tk=8JRvmxK@TzA}R^!@`Uo?uKS7|(e-bHqYp-v+fe4c6Y0N=RxV9h zbNHr51*FOn$fD_UxMFDPs=+SrTID&;t%kY^Hmb>#;&{5hUx8JZ@Ax;srW+_CHGF#E zcRPrC2So>SP+SQQ;NJc~cmWj&VLpTJ&5`T6)uR@1aAf9(DmLOcdNS29)o7=S#vm&H*iwr^SRhN8tm0Z`nFl`v<-??B1;=;sW1xU5`*p_y3p5F?&TUN4 zp(iU!#>~;4yw4hmG8dX-M95^Sx(3P0$pI}5tQ7B~B*o^jqlr?Y1d+~e9*C;SFb4JY zJK(<57Bt)9WN&M~YZZ6x;Ns!sRb8ecxa(V5&3Qo&H!pO3-WGPBbM8Yr<*zxx`F@|e z^fnx-Y4C{O;)GC2EjD9swLBg5QB|ut;EH9XAvPF<&|9Fp3vs~Xp_=+!;YNxKWiT54 zd2`)luSZpLA;R6az-4I4J!fB{kUm_r)z7BU$Zrt!<5aRt9=yw%g@4zg!F&^McqMd_ z$SJpW-IJ$}p?LS&vb2j2T<3^E_MRbf4u5hVOUe;}?e=(-)0>vzNIlbN*sOB5T^p$~ ztRlzJv>nQ7;)@)0l5XP^f8kg*wL$k&=HfGeLf|MI^~j^)`WePVWD40gER1L|l*O`j zgAP}@_0|R7CyynojeJ0&6SG0E(3my^J4j>hu3;3y9#!WOV0-+9Y`T;mQhv+&{eu2W zMrEd%Lx$EV6;#{$7VBk5ol-PE1?Vb(XGPosy;Th(dF#zma@HxZ=GxiQXXh52cm>O) ztM69mQYqIZpk_l0&w7&p(FPn3#inE1reJy({qM-VWg(pBXX!V8=yPkV>oY;YKQhnz z^Vpq&DjIkZ`vB>~Sf-3PQd0a)9nbsB2(v{^!wbw17l2oEO*P%Lq7j+^7`iI+; zbxAj`bXr8Qe2KP69ec(&b21nM|N7ZZwR8WMxtr zjujp{IfX~1d@61|LPOc+xT$Q_^5d-B{HgMsI(`S8iaJS^Ta7jA)XJ{-_Sj?SGf)_? zf1~brv`C^un7Zay>c!dzzZvPEMI5=o^+-@il(GTRWPN^F4h*&{2S~NivbD8!HJm8- zE#BltcjCIn`d4vh%ycwsO?~o4l;G9sn8LYNDGsw){od{=odc_z0If1W3py1I#rMQ; zTkt=JH9;7y6~VQd#Xj+LfXa)z_2v)7sKlDgIe_?!OY}t5`T?(mQ>L#rl*S)S8BMRX zJ~se?V$i#OoT1GFOqV`mO$}m^9er%nw2r_ZN^Vh_b9fwU!L(X^*Sv&7fi}(K{Vzyd zMzqa8p58r3BaY3Xs`G8W4*g2o;F%I zUCfwvm5T-W{U0aJCRQiI65q+Wlc#r`HddE`csbnKS#5Gi?VU5&EdV4q@#T|PrO?i4 zD2Zmxjz^^Oq!y%)tsTc35V{t$55e}oL}H?MkB0BlgY524@}I4Z{qv3a#op7Wk%N^i z7o&jsRNR^h#krqGIdhCjffhv!rYfv|SlX!!*KH|#{JxrChyr$q4J8pBCg$Sji34b@ zS;-82zxdt00NtZ~IcpDLM;iD~<2+NxhEqoPTW zmv$CI2lX)}@xnwf;gO*c_z)0cZ!pz1dU!i10tAWkX_(^Zz1UDc@kExculUEFpDX8O zM$^ta!_?oaoiWHxndWyyBCqE zjeh`xRd6-zLFFvl=Kk-GtSZR9;%M=unQ>>7Y#XEjm|)GiVz{g0t*vrx${0v*$67Od zB0s%rx}<)=bfM>yN}*HJ6Jpt#uH#_y5ywvZ<(PGS$+sHOKALry31Q^(eSJC(n%5liu&8g> zYO_yZAdgyA+smAJ3ctuB9bxidaW1qDp4&NEu)(S?D34PT$t)vfr8-UKJ^jQh8yJ?@ z9?gf|-BNy&R_Y6#U5`~DZG_*Cz6~XXiuB8QOu|T3Gt6SX$BU+`r9Q0NKb#}w=iqV&n#rHUon0aqVDqKhu>_3uGG=z%zB`_~*f6?g&T28# z-KA!Fcz;ZZq*Blm`!jCZR_>gs4?lEd3rbK=@VDHbMOemG-(_vKx4hdX3ltkFC8%Iu zuQQi>Mtr?g^N2hv;#)ifT}b2iXUyUyVw$OO`;Dqef5m8J0y$ zQJziimzmUa&ZTQ2)Txg}f-Zk#sra{Zqzz!WVPHH?^95!~Jc8Q(wthtEj%|T=g%g)I z!8Z+DK}yoioR$F@-19^hM0GEvVhz1F3_m_vCkmnjTYS0$QK8fPwIA%AWl9sAi@}8@ zfl_038aB#rkw9LADd9(~D*7B2?vx{b&y1LT5F|9EP%JuKt<*w`QvUnUT3I|Rz}QHF zulJh(MaKS+J6=M#zckOL%5hp79nJ-!=kDcM2X`a^;e`RA$>jTZsn`4YEwXlsb21Cl za*{Q%S6C~(8W~rFN!S>*zJ2sksRTtf2?!Qc&S+{Bco+$NfL)nue3D)!%^<@nJ&zUy zuZ;?xn?uM$d@3LGSE8R((g>J_CD#vINegYk#ksY`A@XYnj*jEN zA=%8>>8&cx8ms4nPKf;!FmPyn@P&F0X(K|zK(U~01?8%c1OwQN(@+E%b8|1!2zdI1 z8^(Z-qtXWzDwzhvQN?51y6aQT&vgoN*ut4TGf(IRrM{0+GjCbE3lsx0Bwvl9?ey)Y zC?g&D@O@la>)Byd+44xTqSh=64Pb(B`d;bN7AQbnmLC_0a0hx5n>K~jgk6AyI{RfP zDk)md%51u^HziAm#fr}%OVXxQSIe-B0v=1LdPo^Zhx(U@UPk1S1Hm1%M=fMGPOD@~ z9jT6So1+TfiKzr6?+ET|kOBL3wIw(()6rSz zIJT{L*2**!`WQ7B@$g{0%X^jmo*O{pZavMAE&?NPt+Rjsvv_Pq6`#!~hqT2Y`Oc zLUO8lLYc_(`O6;K%3^9gB_eXss#UF_sfx_dyH7z7jVkxXrr{vKWxgEuS#^&=s=8y4mV4@g_qPyx3 zK!A!rVN&+DIFwo=lapjxO1QzeNa4vLF5Vg*yCbFUQ|M%ReqV}baJZNsdViuvOQkKi zqx(f*w+u$TC%(dR>ka$2beki`%ERA0YA@VWxzjV>{|FTCcC_56`ZV9XlD6ReGxm@+ z@(I{KL0|jyI)eM#!SGP|So!$>rzKJ(@)=_N1gu1f{Qt0$tKENSuKxoj{ZHJAiOmlc zme$$s|H_b^%uP&w%;^8igEg~vre&sOW@7ul#MiAKSjn{~C8vO&#t_S3Vfwh&4@{L< ztMM+zx|(zr6cawoaK)nG@?XVvjirM2VBaPQMmtnPBikCNxMQ-~*!>Gwf*7Q%ic4cA zUqUxH@v9+s)z0EXttRW0@SI7fba3e=J|bZ3om9`@+r=wM(V zLMQ~p@HPlv5gzAVw=O-dr_Sk9w*mmu^ABf~dT8^fz?i*%AX<|hoZm&%tZ7_3Z@08e zE++J>^^r_@z%p#;vRH|tL*RMZ$7yq@{|<@3?)M?5g(%8g9HBDEeO|tM!bqFDuo0Nd z2Ig%W>vV3zer8~9w*;=FJ8r< zBgEj`HK9_9(v_VqpzzkVhH2)XnIx1?dD=E26G8%3|Q6)on9?4fjSo z@lhkzc{O*@pgW86_UHU{OP^D}2jNQAOGX{z4!lB;NIMChFoB_S8S2Pd|uPQ`4k z9qI|t#Xo)GLY(yN_>by+tkA8dnq`-68_i6)MsrjtP&RI8G(4?OkZYmyQQsFK>-Jwb zq1WkWvHrMy-W|Jru66!7 zQz%u57Uo>Gb}`PbwzVEAL3;4q?#q4CO_de0V?KTAQdlhh#J{L_#jMg8xj-En=itaz zlA$9Bbe~4_Ar9GJo4_`tGTqB^{2PIiWmIxsO|FQeqv7VBsz72oSc~{Tv!tu$35DUE zop^Dg08W)G*q=UyT;af|xgadNBT$BeLq|2`=RcPN+SMzh}OUD>c{$ej(E+D)pMKUg_Llk9(a%ZgTk$7Un-4KV$SE@T`_Nm8i58mion z1}W?V;!^F{Rl;Ca+)kN&D~JTG9!8z+D9}7*4~s^X%&HdY?UcF9dFq9edZ{5~o38Hkjt*?0Z-`@qg zwHJ;0kU~4p?^f8lLoX-EnBTUK;9LhZiQ`zYt@!6xGoSUVDeRSGiAf?NekN;f=?1;J z=qG+gFYqYUjXjU}Dd(pa4u^Kn*cMt)e7?{X8To;vU#y9@x2-Lm|Hy6l*s3ay=Fy3~u6 zlKfq}o?l>3PCGm$e|i^%uudeVcQ8iZLgzm#?BSuh2m56?B;5;4hAGkF{uV}R6bCva zk2trxqU)Rc)uUJLMA?iIIOf<~I7p$HcEN!Mh8@@Uap;Jb?k+i!9P=rm3Z3dyUctVC4RrMe>%HvT4eS?#cjix({<0gD_{F( z0JRff$cO!XQ}p#c2c=PfpEkn+>6P#Fc*n6Pct(S8MZ6lQ=)+gWb4w&qt}Z(@I03?> zPo2=Mm_26k>i#z8Y^T3QeMIJV=z!P*Z&K?_uV3*ZBxc>&e;NFgM0acAQ@})p`X6)P zHIVntcY&*5j2GotIX&C=(4&4^fLA^qfk%6q422O^8AQsrE#N&|{e)YF0PfZ;Z!H!g z%)*>m=Ewq@?!_!V2a`TU%Y%T*o^Ke+chxYKyqWY1Ld}FC*vP@Qz^>L-uQOk zjY`sbudm|)4iwAR;lLl`k)$1%jGTkgy_2I>c_!IVCpy#8qt;1+NtUwZxS}`B97zX| z^d8+3GKU6e*jma}vmTL3D-V;`R$AiYo<-n{68VzCRc)EkirKM}3SDH?HD#>@_Q@eZ ztYh~H9~b09bil?Ba{p1CR z=*l2DqM=OKw8x{E#|`Pi_sS*w@-#$K(|`<+_nS#JNlHYdkOfO9F+dFg78~qg5kwq$ z7KtYgP?SVc1*U6Ri*Dc>rSuFzxH$RoH}ejY@lU$sE@ppR~~wE z45Ll8>E?gB_0uDxd|WrmWN9b~@e7l!kNLx)jJPYcuEw^huC?oydN}oe4r|vpv|O{4 za^eY!jDJ^fwODb%SK=MTG$*TZ!P|JT_xmtIB3OcwwbJ-op=v2Mh-<_^Jj{7^EGj-c79!+ z{=DzC3s-TK)rtuhiCWy}2qm8BOHH)%iWHY?y?WCrbJO66A?*d{G)afg2_dGiHtJV1 zL+N?5#8>-}ac-3(;?>wY?wTn!{pq|6OX%Er(&~+4wrni?XWfYc>!lIg)L3SV#SYHj zUj+hyOOb5*=}8?6{2sjowT3qSYsp6iQ+*Td=~ghI2OFDjx}BC?=>s5c+&3_}UnmgK zL|`IX6v3hp5m=QcCRFn#WJ2<%eNpthWb_dg+0=zR9^g^Gy+-zj(9ZRjR}$J_d&%+x zMke_L7hWjRr^|MIgJ_TVS_f8fu_{i> zSarWt%o|#QjO-6rhrl*2)f7iLoETmwPCDJ4o#>4PqiRo9u`3E1LZ|tyz0sL4ZBQBF zMNXcWvE=c63u;fPGCtnDrVEk|-p>OdlROE8mI>^>@NH=v|nUvBTeA@44UU+Ws>Oz~b>w zvbsP3RImP<74r5R2-ORvno!NFrEu-BatXf+c zC2TQdC7hLpqK_l9JE8iC0s}bNWpW!$-DD&Y7X}YfR@t0wB zY$Q%NCF()f(HZruEU#2qO1Yacd0dsVyN=a)^Qh$cW5;Br0dqzSgDfUG(-DL0^{|3E z9j2u@i*rR{rFcU~ip5dzmWuF5{{}IP$^I|g$m^(YHvg3PU>INy_-R!p>$=jB{fuox zI_I#FfIV~$&d;aw4vCs6-(J3UA5QetC#2*w_Zew7xu_wY2LHxr%jG%-@JUZ2^a%5+ zqeW(km077s=cB!f5IbjJ=yzvo!|5EV^xk9S=Bo9YW@od0Q7!+9;&9%jiGuI?IW!Cg zL@yWun5M?WA5F=Sz>*-CNWk_^ihZP1eEfx-KjYX~d)Ohv>xi~mSjx0x9hy-i8LPCF z+UVgc)sP+fotEYqOZINnW{y@i2N^|!83hCNen$`tT%-IAVV;I%+Cm3ef4gj>>Rk&{ zjan|!*sIeL$v>HFlDre-bZ13#|Kg-bjYQPzIvK^Tk$81YCi=Cn(r;li@tXEWE<3b1 z0Ca(p2UqI3aE5QEq@8`Nptqce@JdanXk(=Bk+lSP100-&c^*Fhge`10LEK8IUBw<{Lp^Hnppl3nLj!lTgHsteR~>YLdDP4w7tl`e zl;nvFfq~%oG_)R6>RJ!E>8X4V*deJfzq+^i2G!zgI+o}lM}=k5w)3C{NtrgbIxgz# z3N^Y$&9w;TP1cxjUv-Zv<|So5wA9{du?l)%SGU`@6~c`w3fepc-u{(_teL#iHk{+9 z)HbTFXwqKD^W~0udc(s{$lfe}Evwod$R)GXXK%bYo%2r-b+?s&fAHF}slG%`d7la3 zUpT9B>nAbtw{^Oo)eg9s<#C{ybAXP_!bK;7y|UhJZf_3uzJdNXjm%99>`k1UX-%yy%*=mK1_KvoI~xOM3nK&TA35&-xS<(3{-^1` z0EI2PEiDTp1N(nH&@{E|aK&7G$b5&#tM=rt&eH#-=it!Nt0quN)d@opHAn3t4W0j6 zU@}=&epFR)Vqb=yn*@%GX3HEB{6&o8eve2YfP6?uKfd#qAk0_LzQU`fh+=#~2%i~v zD5oRK={)Ot)AH@?Nj18(S*J{fsdoYTz4`4)Ri*)IepQA}sm>_KIEqHhh!Dk_xdmL^ zU@1r=2S8{FIC6zuVUg)SnPPmwG}cQgQjRETfzh+Ev*Fp=Ue9Q}D5;7ypugWxSbQ$D zpSwNqS-mTCEyWS({%!NzA*^pjIjutmL$)h_{}T__HQ&0>7|Bx2TaAe69 zSHg*BTgwU$clW1ge}X?Ios(_WKvp5?8-k+uDSU^$+LgEA@sJpoUS2j@NhSI z*}GbNjqE6PGUjE9{l~FBD)@y$S@M|Eh-Q;pT*kl{1Nn&>hW@2Q5inFA(icq#$D6>B zf~B%23AMYWz!W2qJ>(v>1Br&wRn2EXffy~SC*17CQ^7}Kx*1s8*t%v;pE<9ml@FjZ zup+Zqd<{6X$53RJa*LQ$M7yRuxQM6VG>zN|jZ;PkB@Au{!?MY6vO|>}Y zhesWVM!q{wrN8T;N5O&&31K8G^^pu7U5Z;un#Cd|+lI?>s@!wEkeeG{qWi(-zTLka zJ-G4wM7SADKc|@xO!QW9EM1Ox9g;Dm?Z~kn{HP9-+gCj}_(?0^peLI$~>jMQhIvvcGP-Vyq1D8mYpj1`J znvkwaW&R$!uC|ZFNE*i9<)s6F?o4PQmM%l_H(Ftz(vo~NNdiVy!BJW^OF_|ipenP! z{HQixgtI-I6>FB|n?rLXm?LyqX=cW#nR_QPwXRk%Xiy`@x$Hot5O;9q(+eV0EoHLz zvs>vQ7YDPETa2;lnDH-7p4()~3{fIvF(8)jTq9yN^y*r?Nr0f*g?Y*CWNF_BsGWgGld zmh}t6l?ulM6%?-CrT=t=sfEwijpzML_opefxroo)<3>NjrF@OQGuc`Yd7g^Ftt9CS ztudt~2*JYMtSP{NTwMQ#JAZK;R|GrVPrb67Fo`W?{d>bVsPGoH?a$+f7f5h_6(aTd za~E2s6NqXjyERk*>IgT2VhuyRp(=S;O?IX*0|^Wxlh!$wsChPu2fpMa3+KB#{$5T9 z9AkErKp|xiOI#z;^4YKQ1Fl$4x|d(6=@qg+XXm{@vtE8JbMqXDjh-TjVSO>m=%(iV z>N+GXb7=#WC-nLe08>UZ$P{kf{q$}}i#W@x0bNS)^fx2+JYiZfy9ia;-zV6)eU??s z)b(fAU*U#InIChswJ&|LF8{P#iENX^d+xwXwf^zy*1T7K`_ZEkqYQouLdt^)1VcGE zy#3y?V09^=mv@qq8LvPEnJ%eh1W*9#B~$0PY?a%dz?@$1!D$;D;w#0F*&T?fmQ}1@;Yhup!Nk*fmfthREiVNykLL8-zNgSXv?8&4e)3S*C5{*ecKRbS|N zP;4vOVGeeE$|W0rHZZdD9R5B!XeGTRV!6CW2lg)ty$F#f{ra!C;7B~Rp<{Nwewo^e zEG(*E93wSSv@>9~IBm5hWsQP!Z;B)DWlYjl_jHL$r6dYyNye*`&}zv2&Q<7WQ{s)7 zpiIlT_7W`1U2&A-TM9~i3L%nmT&8zJZL0-H{Zyo|mt#GfQC(^iR06wLv0ybOwF7lX zbYa&FO2xTZ-_nKak9M}H zC)2^i9#2``QoxEDImt`LM;U5T2M}n<81$5~H z7?^`HR7fmOm|UwhRj2Jqea{$I1CCX%oOy3lIOx^=OInwp8y6Fe|VBM4LzGBKz(&9UwA5eW$7@4>EbVR z7*E6uFm-H7KAgu}Ki=KFZjTH%nKdy8$t{DgblJu}0C2#3l7&OD5C%KfLkg`c=j7eI zPi%3G=wx)Eb7QnRTJ7M)r#5v>z*&sfGyVGok3N)3xTJD9<_a5Nl`vJPx|wB*<792= z2UUV7(J7W$gUl)t0w&8>58r;r5wxmzt9Ry=>+NpUbqCAMS+zH`v`w4m_s;!Rwoxz^ zebK`?_XSmhM3&C<0V2pFlsLkF2p}|e#j>CJVl`HunU??6lURnH{oeEwS;JL(lF$@>n; z>_EQabtCn6Q3>2-R|9bf55=*z%i+*wPYykE1=-&G*)3n^LXVSmJ>@^Pvj_uV;lgP1 z=Nb|r2Zr2Ao&(k#Rcp=+ymVgr8Sb3);RM)=U(HApmY+J0a2NqBT+zQ=W7?$4iPrgo z0Op{%14yY`r%LP>qH1M~XzoF`shg}^SGt8;!P)eb@kwZS-u~m^9l&fOnlg1*%$t1d z`dABH>+%w!j@nYlQ`Nqq;|l$v$;a|<2ngpCe4k5Z0!H?>F70Fc={Aa<6^+~&Dgx`= zh4FGTtYYK^MJgvKtOLiW2VMVIc-YjFzh^`4)`-gTq^UdU@V28>pP_J2ILNp&r{np{ z(v+xNe80J=*z!DibEkD8J~S-8WU#(dpp22sYls)h_WpKx$BFu6iSqD8C>=Laz<-iljaZCu2KrbrfmUsbxNV=%b)bN{K+->7zUnx8 z9nf)des_wmLesq%91g-=gyJkCuvHP*s(kmiS~|+``qh!9sF}2`hJY?JWcQ~6EA9; zL1)N-d@7;tq;yMlC$?_uSp&lB>vxn^wk-KSqqJ^boi4E!`|FG@la^B(<4D(U?Up>R zqR52dE4A5P3}9xmF}ZTodhMCt3W-n}*?{DB#z(8+BhMw~WoHFFqHtYM&&LlD7bjUD z3Tke{3WLGmNwf+W=|7CI09{1vOd8^3|X|}YPIXX!$n{COp`KJGWt10$R zwV)D=U%!%s{rO(fYMyk12>V|z+O$Ij5k(8$cql8Js}`6NwEma^6I2{cK(vyRQb zVGPU;8K#0!ArMR#d3ibRQPCh4uoF|=t~Wiro@cT>RdQ}90x94c0B2whi`U>uW)5*h z5^T|fe<}VslGvB_C;4UJAjc`y9X>PD_bW49HrO*A%7feX|1JjS1h;0%mlyrrJ0{Ks zzd-^$sJ4W-xw|P@rA@9Dt&}oq%*tW^ES`dtrT7Jge`<=*0=vV zh8S|61P3$;2yOs?J;yGijyjU_muvcS1}(Jfwrjh%UUSUCXcySTpIAe;$3i8Req+U6 z=nP=o#Rp5odI|2ZdI4@R1OXIOxCb}`soS=PV1wzux>Vg1-UED4uw^}Y&`US?0SErZ zFXqlebPX(;Y&2$$Kwd_0vu6Tf$X?s1XF9K0yZ!F%=zMc%Zz-J?a1eRW=eY?G9HU{S8|njaWVpa7!S_-{`J(2Jq1>p$OXDYtG^zeG|;ar&46{j51zOGSl)Ks4|Fl>FhNMJTwi$3vN6|<#j z#IGLCqHGp7SkXFY{YJw`MLBcV#cE@cxPyC5juWxqN}n6~cjab397AQJuwF~vV0a-a z_tvo#MtlRVZn3A%_%IxG4DJe+R^uyv52ll0)VeNYX<;Q@MQ5OPi6vOe7Ujt;?m-W~ z@52c#ii86`h*WAF=MtJ^hk>bb=>ePK&f}wc8D8*38z4torz5xpXyBlIikR5{&c5V; zv!mcr@m`zruD1C0#UYT>%qc+X?JH`|;JPix%l)vt-i{%<*#08Zv-RxsSu@+0^X<%_ zhib2COE~&yj>9sKgZ+_21`9YUv|{I`1Sqi$ng3(Ic1bwyY@_~`d_*OYadNjY2X}qV znD)yK%uybhcMa)oc$*C0++iN4Z|Ouw6@CHsG$7CPH|}z> zva{o^_vA=1BiH0Wn)!3~M{HaA)0;&ex9OBFLkU=h6;H&Uaf*ybARuHjvIURY<7!6O z@TlOsu<+tq?G&cfaF4cq#MINg;0j4yskFMgswC@mSXgGhVv!%_8E%#@Y)ztk%D2#) zb*E&=Q=4h4t8W){pYK<$J2PDiDV{Yg;Zgg9TeKxEPgGo-OIe)gv$U74Q|4=1C@yx%X~l7}^7F4KaxH$6 zwK*c$?Wy(j)>cRmumT6RBUW8p+QBPRNAHl(Jd`N3da&JWpOz~)O9p&kT!DViHra1k zAcySHsr@7SG>>yB)E^rp3B$SqCq{n^47&aM%kxuVVu4nTHxRcZV`kj6-Y5Ed-SX+! zm<|>k(9{3q+TPo^I@@3#rzjq%8#JvmwEz1ml-bWGDQdnB$JOf$ZrDe|-ttfHb_I6w za%Bg|@n*H0(=`gg1!PAj=tTa`^>e7hufR5pAwuIZJzQa$NzZO(Xo!!Dm|vgQ@niiw zUsYY37uzM2eZbf-HUd3_dn>|mzQm`GC5eGq>uW2*A8#uvBF8zssI}h0W5V$Cr?^tZ zd~b^nZ)$eWpsy_9Ph6_m9pEl@y-M;vmgOy8-K^~x+Z(-?sE%)=wK}d96~JOIC7Vr+ zTyRFq#SVOVp8}Xe2AgmpLik{T3T(Kro=7-P2ee3hs+Ji-U^|FGc4PM}FE_W_%(+}+ zqYAgeu48-xo5j!NZBl4mRa8AfqwpIS5ae_VE4>D~zu8qRjTtUU9{uFLO7*Mb7Qn}; zpjiUAeEqAg9Uyw`C{kAnqkUOH!P|x5< z<2-=v$OinFG?>b)7-F-FQ=4zsjg>c^p=8V=!2GR~y0*Mf1Ma{0Q{82Gpi^~L(Ow;! zC(D(Ox_v|#g^Uj?3Ie$=5DVsvXbMEokj60LrITUf@mP*_=`Ep40rOYPPmv9U{NK$+ zUaY<(;B@D(9Ul)ju?VMz3ORHxS^^c}!fa{;vQ#oJ7o})T{+?{YHBG^ zMO5ri)}q2v1Q(P%QqgV~3pd`Bfil>1(E*Z>?9o%TXS;hAjqYq4cpnNPfvyO*It8GXzIRiBhuEbt3 zsY4!I8Y2DUocd0fR=$A3mnj~IBNND>uLm&nwhziKks4$>kS564O({bMO6qdE;Bf6U zMgfeIgJ;bZk3BGO2q(avg1QfPSRypjWlOO~_};tID>v670i8ZC?-biGjB$YyDcdcz;z4W#wgk?OG*7ijDu_v8qQf{v*QLly z371e{tb42qx>|!=SHv@&SV2bvIDeVCGa8F4&tbu7RxkdYBL+?ssL8SRtX$tc3n@p*$ zvF-4+;30>E|0!~CjS5FLlB6+*oB(lASaOQM)5!yF7$wX4J&zD$o@fyV^QG)yupbza zHL@RwThHK<;BkZ_VJVAMgADp^v;gu7I+u>72qS-~@E29r6on#80aBh+6)o?eN??zbWelB%isJ(s?lVeD z?*%O`Mk+_QcX17jftSjeCO({*FB@)fYwwk=K+`9jn)#ia`!~ZksFG18=6+>INeL;% z!*t>%zCV+zttU!ce0|n#jY(c)zJPcHG29jjs$7gyGw!IB;lt-o+25cZ9qaKb9p(d0 zY@HFD;K(K4rs;yxJtDb{RkasweRv)ZuI&nRGbNTt6Nh1$*+Deq&%CF~7ih0!2o{2# zWHtS?wT2}@j1i31Q%sa)n`l_gRdTAB_w>uh{88XTEcU21OQD#Bp6_CtBHm`8z3ceF z|4!Q*k`)1%lo?5}Xu^O>NS<#GfdVvS=n<3IIWYYdCgxz6A)7R`J6e=L!-K%xl*64_ zrFiQ8m!&JL7bdcHVOJY|d-X(=wx=$FkdnUKqg8fMw=V`;Sxeh= zEP$jYBb5p}^oKtCF8LkW;0?PKp;x*Vp^t7ZVR)-IM@Vm`pA{`>fWfHJM{8vnBsDSt z*ot$1=1>C@kjt2bMqaCtq6LkX3r7*B16FOC`Omk+l`3Le{;n&QMf(cxOlv9$c5x=7 zC6L6fs>;GhZ=dz3T?ndRVqLF+19hu8w>|{>MK1T7 z-iIl^S4j|=*|+5QUJ<%;=%t~Cs1AK-=+db0_RSoHP=?U*LKFr`MCGG>>5s|-agnGx zW+FDW4h=e9graGUv52-aQEZiltt^mL3SP-EglS0xNGZDBf$ZUdV-4Do&)3Uto`lW$ z=w>F$t2|~2GkL62^1OHVujg5=g)^X^#enbSs56*5eN1d80cG*&$@vgMNTXu0y>L_g zwy*n1y|Uru#Y-v66uxYgdMMxtLbTe!+L+H(UH{5I4k^ zvhB?5mJ&Cx+C7LFhU41i=;O=SjBG^lCyfj^vmbus4XY^nZu%d@1=}R&#%=Ss`U|dd zdds4^*z@f$KR`0|7Ivb5S7U9&0rel1-pDaby+v(Wu*tYG-k2Ukp%(**tRadoZ?Oo6F9$>KM(CZ4C2xMY|*qCRNR)F1dg$QbL zj_##_TZa)$hZ@A)`J&7c7ZV3=m2pVca0G2(VhJDYOwTk09k6c^xQ$9Je&I>W>3DSI z+wy8}-?%IP!KnFL<;AG=Xyx?qSWNBHB>ATexN?au>oTqsTduwI&!o8`t&fTZX7Tx5 zfo(qHuXig-*L)~~X-?Wc%rA|9f9x%`Q?>6q+?I=z)}!B7n&Cne)J@xTt16VDRBB|Z zaVn}EQE^*+SPN$ui=*!67!5b`*5$>8WV{sPo**>i$Z1V^!+|_GilSd&Wh@@ZQk*l> z=ABZ4m{zf8NkbJ?RE;<11e842H-fSX(%a4LXs2_8^x9&E|4_PsM&*HTRPp`_c$~rbqo6zG^kQ!;dpB5cF-92AxyTion)Aq^zSNCsk zq9zu!ley|60c}trZcP}}>Z!p$wQPpEky8PB0j!Hka2<>=aUvgpm1kHSl%-;}t zm=(?(Iiu(w>!ZAjo3$gwJ3Cj+(G+)_pV;q{uxX>xz${GJ?Y2QjAbeGqIo|Hy#{$V{ z$nh|b*`uVVb(JpgN*)knWpymEAS`4Izfb3QtOMP1=oH7K8UcB4Ep<5k5q8$==Bar2w+Ejz`lz@Lg@Q>$2;=Ce39W#elJzi&fcf-Ct0kLB?xa<{Bm0;=*ZBg6@1Qq*_Q=0dlCAg_T< zWVExurE1FD`fX1C9NM{~q$Nce49hAR%x-_HjRTItK(y%6Y*0&_^RtG9^q{IYg6-ua zQwc)a`%x##nY7A1G8KIPEN60#{jp5FY8jI;ZWYNGUq{(s*#I9mjwyJgfC4*fU;9D& zm6Zs$zUfLmVGhPbHl*Vh$Z)Eh1kDY2kOzo&)Ym~mghgTdObMFG4#MMv{qR7OCAh8ufl>rb9jU_0 zyKGm@F?HFS;Xm-If;;O3c3G%VBcR5tS8>@ntwT0?GaeFYasJupq&mvq5`1elsU*%h zFb_f;^IK3vgDVNJs=KZ+@Ij-cdTiG>q7$UQu|aeJQ@=KAl-?lHL)sH=+hQ}N6wcHG zheT@n2}=U=Q9cAK>W9B90X6Nvt@R5AjDZYbs-4d5#gm3S7$tG|wW z3z;-u;XHt0H|!H@B1L5uQ}Ll8fbL@4B{rUfA;*@R{u$1%od($K>&)VeT^ErFjgmo4;rv6!BXoIpO+^0da~g}RA1$ToW0S{x8%5HW3>7z^X>&PCEs(-zSmrwds@}g$c zPAF>?r%tjp2cyi(9kjBJ?Dd(^9ZVI|bU!>e71yT?1}b-$bi#OW-p+C}k*P`0%m3Ca zF2BW3pE16AsZ&f+0~Z%DXj|jn5^yG7lsU-n3^|o^(yh{6r(DG~q|m#x4s2XIplDcX z8o6R}oOtURlem-)bGm>EnqqV`AQ$LT2#`Ox1wZjVjh3u~xnV|q)p$%K^qvy4rJm-$}L$cg_DK>W)~2uB!Acu*MQu|=qgX5^Bi5>sS2R!igW zL`Unw?G(~-3?kH7WeY+S=U@G!-=D$#5QFs{$?Kw=IA{7q6NEzHS6f0)NbHqzO;%Sm zdT%vtwgw_C9Tr-f9J;!q$&0%CRsz(8Yc{l|j|mkd(^nG>Bc zb1ADIdwFkvRVh8H+JC`SN0zBh(FIkVMYg88F0oO!A}+2^K_CXoaJ`-`aEk!Z4NT5Zf#(3D9taflBBieiPd1CrOot*!c; zPyt|gwlC)`QgUHqDsPba(a4Nn|^iLS2_}FnWKCz za=E11)M8@UJiIT_>@P#b-ctY^Eh)Tf4kRUt$AATTYmqd#Bfg6kBf{V=b{R_?_xBH} zA3O04A2bSciTyq@*4yCM3J^D0&WvD)nFzn&$!yzFs9P){-`m5>TwxN-2wUerm+<*Z z1z9G~!kqbYg#wL?b;a_tHp|jLD1t*)1k{-?cSyVbpa2lFLD-#-5OEHIZuGdZ%_t#= zO+f=t8d8)6(@Aba0bmYTBZk|4pi?Dh*=zQ{Cq0-mlAooX^Zgobu_NQA>Wu6kIkyR>j! zfGESX>CeH;SG0>0+mxUu-U##GUaY1odw?c^;T>=!#XXvEj$(NJoojQr#?eM905Uut zQ-_Gpb%;0<67C2wX>Q8DqVUA`BV2dw&!=U;4X- z1cQQh=FX5A(8>&Hq&)pl{XivvG*m*AtzGWf`uAT42w4uk-9*q*$a8?Z6Zq(RO{g_4 zWkm-T{=r?|1`BWeCTub6%vi8Y@M)vHb*EhDQCPeg@~F(m$lW|(Gl>xf9D9r{#`3M% zuqcZzFMO=9TXP^0`dIaJUa;xPv~XN9nZ8m043ZE^KW#yb8HGvs@baHA1?C|S++g-- zBL*y3=WWz&C#+zQtnkrCZxt>3a6;uB9N)nEW~9YT##^1NXiSJxl~BsMF44Y=ZB3GLxU1?CL<`f?2WSR3A`w&f2&hvUIr7x+=1nAz5dRv|Cvqxw+eS%QaCFw)s>N7AUKKB_q+#o zHp38$Yw!5l0W2Y5Cy#q`_VKZ30cvi@;TGJYGtW5|t%_Cb7fS6VLORoJMAAAyls8Fe zU%w62Eu*O}((9h6y0Cpurw7-+)ds^nBW*a$h>dl1`*)FvH$G=}RD{WCr0e-A#I~)KSh^!h zVe9qDt~2C=Or0S%EgW(3ow;;=H*|RL1jbBiph;6j3sk>Kt&)ltlq=y&Hj}9IH0;xp z?$CLU+nZ7f?T^|>2;sHNO8eu<4Q#m&7t_4-al5-kKr@A{LyciI5 z;G(%<>ep1Tky{r|6`D6sZTGgKdRF9pVJUhQymFxTe+3QGR#e*&F;@Z_a+>QOQUOT-c^>cqZTkGrBlQ1nicY?zeL zX9&%9)gv}{rp)pvy_EFgmDe0S1EfqbAC9@A6RGvw=n8>5EUJG$@(Q;sAt`RyHIGhO zt>t4zIb4+B%heaVEZ7chIUg-x6e`}dU`(eOk~SR5uvDZUnIoKPimNm6bd+PY6jJkT z)}hbaRI1p-!X>8o2$j5>FzCGUfLw?yl{BGl;F4c<}93CS=~so zad%ifvJTBE);o7rK4irdM{VfUnjz^fSXT}f?%Q+szeH845s*#%`mn^1wP3B$!J!k; zHEV5yVNyO&u^tL^C4&^_Q+F$?bnfh^#9}X&R%eqLvS_7KzgC#iIuPuzSF6daJhmnM zL|5jp*REUrSLk6+u<~Gnm5t=ZEP29u*e`!u?{Crkn=M(Bk>b*)7L*6huwIJCpF)o@ zlR}muf8j{p0gHFYq--&O1&uux2wRCNQ2{d?CCmmVZWI=Gm7F)r6n4s%TOsCvj-v(i z#Tg&yBB~2acTkRS3%jGD0-n)1(Z^i(X>REi((a$vN9wQuoT|_EZ+CO%eB^9-iK_*UG2ImTbmf9Z5%j-fk`^QR}in z5NBd`fX+5b*?)_ZGB~^L?!>`vI4zjT=0fZngUm3eg^}M=`v1Oqmq%8c^y3c(-7UcY^;`!@Xpq}{C8P@q&hJQlP3F2lRep3gWDoQRdxk1A(1+AmKAIu4GpmoG88r(_ z8v;5TCHpdo)fTjj@Q4~ff3o>5Hf(O?(cj5ocJHnXfnNy*tiDaz*aYyqs&n)2ri15%9mD}wa% z1KW4#jW(nD(QLX zk>nUC4kU-Yt~`#HeW71?_YHR_T6&tszjLoKsntcHuR=r#ps^?e$Ckp~`ECA2%|+7e z^Jm2y&u0asa?unt?cJ8w2iS^+qE zcg2S~y1dc~VsWvSk(y*IRM*xYTUwR@p#!SkS3x#)ILc6-;MS0n(%HB${WSzVS|vmx zwqb>s&6JJfcQSMeDR%OuUAT!FF@~1m|%SPHs;G3G@wznGNfMPt57UvMLk(13YZVIxE>p58U z8wU~0@NFJZO36%i3gCVEDV*KW(+TTy^b1!>UH8yuj(Z~8#l^(VROj?f>0}~9%_Z8f zmccE|P#$7tr)~b1|H`bfU&mohsN@Y6wMiQ4)AiTxw?!=HMM`3MN|FEk@!o-Yb?`4=DVlPz`r>oUM;^*J6vo`B-oN~wi1S+0YDynBO;Hd+ zjt2mD0Ssg_^Xh0EvEF{_JcfiWG@@+|m6CAJbA<+ljxj__$sk zpmTOQl1=Nbt{XuUhn2h=Zfl0M81JL))vJUmL(5L;b|uaGtn67zTaa6<$@HR`wtXZu zU?r4SEy$?*MT&eU5jm27u_nS4j9MQ>22Nniw;W2XCV;{>E;Ooino(z1M4=7L>zEj? zWH2*_%_-uaIafQjA0RUvixj3tHr_d1FpQtHE@!-^yndJ%oA#ae_jz?B0=B@T%?ywh z&Z>H#blWm{;{Glyg58hTI&h8q7uUn!7rAZ}vQP2q-F;VMy5_y=2z(tYK~agV^q%_By%yZliV1`&Ro z-z=R78gG#T0;61str+!@67iURyu}=7nq5w=cZT*Z!)BrBf0AAyA=~V~T_Jlu)sAj= zg$z1Kv{MrOb#snngKwQ!dC^iw<0OEZSJ@o4?LEIh*I5n#TlYVL@9EvKQw#8X@GS;~ zaqiF9M6DlP*KA8*129tMYz(Yr=mHfy-kG4~Ig(dz;i4r;zX&j=Xj@=ZMPL&B3tQRj z@thmlG-zp$-~j>oo2xSZVKFiPJY^CBE^`K?on8*<93?%|O6^@=&!=lVWK_N-bp=Sn zeq`U_&dW(t5k%(xrOLh!pZlXH{`7cELfJhO?|NN--#ot6*bMwE-3KRLmr{;*LPwNE zK<4h-RrlQiDkd~^mi>4gw2F;YEB1_p1Ipqf#DnX#pAKq7B=Gxct2mQ*D*n}-z3x*( zWIWHYp1yYp6tgCpa8*NI!_D}paML8r+v_r`1ktHe?+pzZ8LEcfS3!MI_8j(M?SPI1sC3L1(l(R9u|73`@wblyUF7+SEr(4*=!U?w5lq zMT)eRylPeu%jCD6RKnVYODnmQRmB?wV5(sDG2z#6O*L0>xqd&D3uwbEa$DQN*+7h` z9J);d?E!kSt7O1=V*M-~gXyJ(tw|qQaR?9eanHbqJ-9+M125$z4@{Y8@E*qKKhqJ~ zNhz4M###`fsIGcUq;K;$NBN_J)ZwL+rFzzZE6NI!7WBdemL$c_QmPs&i<&Lhuyw^> zJeSvz!PiKoq1~_DQHx%eAW>cGOH<8C_dJ~*w$^)gfB zO!)Ku`b6cAuh9t?R7M6RCbS50Br|fNS&74r1qslg5*X-*Oa}2?LxPc!e;SqA$0Wv$ zs_jF9JK!9a=#DQb;e4b4FHM0&m@GgR3MrvEWVVvX-nMAJ0w?I$qFi;>8dZwTI${h@ z5L7$(45`G`am#CpwQYQE&$O`=9i+r4(=jDEG}S7XqXvf1VN9KuQ%dT8F=shcRh@}= z786a?uAp+0C6xHQ{6*pOC?Si5zJ-u3fz%x3&5ZS)sX+&E!6khQ04d$cWk#P6_kHHa z4VyI8W8_EOkM}^yiC+aW8o<;9CJfxiyozZwaKr;hoj9lHgnXcJ^+^iQuL4su@q2}{ zHQg4kh!hJ3r_+}2CSCy_c1;7wl5ZkVHqo?h9e+tLickty0n)OyOA}=qrD<%uint2d zRo6sIBjy}jg5zqco63tbjg2`;m1^XBp^i1^{ZcCcs|OJy)u}6q#my&Hk(rpR1#96! z_r~njncu+*O5KsZorlw03ESVOmE%k}N)z%_h54rz~D~W%9>n z<=r7S21I|-@a@{$E7|G4oSrA?k(AeeQIZ@{C2dJuiwE=v5~-#NjX{Z=frq`tB+)k% zwMoDzbMw>s-wz{xYnmwNy90iv9yPrmyEEU9#{w6lZ1nE=Pf@55(9smRE0oW5ZXON~ zhBrs(c-c8RIt{oL^85}L&tA*ReUC0Tq!;^J-y?j!=qHG*ldnv6!_rVsU?asID?;jr@PSP zz;EDHL%hN)@~BB6l8QxrS1@==Z@-}v3{a;1NA75Dih#eBr1_P)F0VW*$KnGY(7nAf zO_?>tCFVi6T9uQQf-(KTa`xhP8h;Wm*#w1 ziD2^*UMF@kW&Mz~`!FS;mOa5@U_2K2KhAT%UKol+4L_>bEcP{=zL6e})35QFXNW=q z=(-0QQ?5C|q5lKgyosaCa zp8Qr!*d+yDGkyb3b4?!<==T?O^P$FCB$^UOMHO%W7d6<7j4R@-3R_Jaxrg_9g-jgv z>b1(xNv%gnF-Nk`sn6H|)Y`P`jKFA9W&iPTKc;HoLjqf+x8Uvw9i8J>6k5SaAIILE zeB2*BviRxoG?R~gZs|m+gT7&%B#@s)#)7?0_xV`{bHi;2z#x^>MmE2OpMHqw6p;N|{;=3W&OR1`)kIztCce+;UF*c#m)@@hi zv6OI99cuPUG5k$le*DBLMrta-p))nFt5mE&@sVu6nY)-Vcu7t}Uol(A8-r52hkYYi zYt?RAx~yj1x=$HWybjZ&D9+SDj-9UCYSi2~TP6HWKK746tQ-7@r6a?<#{*ACGkaLp zmya5l(%_fyFxdrnti=Ovxivj_UuN0FpQS_meGBGw=HswhjsSSnm}Ca6lLt7j4Gvl1s`#Huyz_&VLrq)O@q63Ze~<6WTM+{hOg)L{5j1zSJ<7sO-#@+W%B# zRaHb3o0sBkLhp&fwI?<4@dv>0e* z+sbd|312gNb0V;}CWHHu`Ef+I3Pft|wQ}>VS-G)QaFVX6!mYI=*OXvKY!!CL8)>zO zkC!r)?rLNWDY*U(rvdlU|GP2F(d?s0B5CE;^Cas5P>ws8{@goN6n_D=lug+zmPnf| zl@-8S_WPLd`yl;2eFPQ~sBRY{w|hV+7gI|wPt9R^qZtv?y2)gC&9Fx93$d}Nes#Rd zLPIiQ_S*71Qe}MV&Yv9#+v6aNf~en;h<;}PyLITnTr!4e@G$o{piP_lY|R^9aUJz( zFZz+aNe($U!P}-qVIb_?a~gKrkJJ)<G@D9GdcDmb2v|;2pqZ^68~v z=^5)hnG<3pfz|?@MZx1nT=>h|$fl#U3huU7{wigLrBvlZch{<@q1F&HF;NYURzXvs ztIMx%x#^$&N01(<2<?0_CZ}6FyFd}_FhNszJB=wH14Bv2*cmGuqrs4$~Kz$CbVOmf84q z;x}2M5NVO%-p9GFWTu^TKWgxm=UTW3b}i?nrjSL*^T%1g-o&Fo@x3+UO_;C!H{%|V)C)wg>2F3Q@ILamO`hS=O zXYSNhPQ=8?HB{UhJy01}6fkG(Nwl@X?*@68tA5yi2bzs@%2}ndFr6PCW3cR zq9&N1H^x@ypNZAgAa<3Dk9?3icnO@tzJgk93y)AwJ~QE3*?E`XqQEN|ip{xy3QjX3 zvH(z|9-u_%iY((7P+U@vVSX@h;XG9XbtPcy3@-22{`ZCoYgJ|>-WlPk5H+DWB7-b)QDK3M`36;wclKY@osy

f1sgv*Rq-IP@L|l`I)a5LD8;yD)%K96VYDP~!vv@__BfpwceOarxW&2q zFN2SgECoM*hsb%16isi)bt!|1Q_orO+s~g8Vp1^aZyJK>_Gkr7Ih*VOu zuwj%!+w^;%tQWGMBQ_AA)2Kujid=ZNxDVq6qFw+TUr&TXsFXEQc~u}Z+&cA#EUpfe z6s1XiqSZw>9WD^D3X`>){1%K!Wc-)|N6v8D3}EUn&wC(UIE;h4aGd~2;-pehr7unA zoon{gNI5p^3ShToT?A1!YnY(>lkz) z8SD$8emm+$dS*OD_KjmHes--41PY}?ZpXjRT%92@2;}8*2@|5K!V6DX!YJXu0xUhv zGN~owVO=PuNWh6nfMUEw^7ihelm@EI=lk5D^J_iAp?5!C+a|vYnNxaaRpVy>oX7M9 z9F36*#5)YLfs*NUxD6%6s}QMe9J#Z7A}ij7L{TJVsMAUe&Lbyf(Iy#lMa$cvGUNR-*h9qu+!1I zk^~t9G;`4kJv@UB!z#?IY&lnIyk%LDAtDZnB}4kpL71c{vmkaH6gaAAS`7ymyioW* zLH1$*`=Y31)I_B~ll|;pSpFwwX{@!YXyQU*12#0^qQADVbZ;Rr;UcWa39!D^~NIeT3Zt9)-MMzX}(r6f6YLx&e&dd zQ22E zJhb%cFq%9nW1${@g9(o~q=~ON>89!c?WY73mzz=BQjg)NUE?%S-3>&AnC$IGfkY;# zI+?jV%Od-N^5Ul8Pr;83)6`X;{OsjAi2?fhIN=Jbk?M>z!OThUjuoh3{JG|$n~7U` z`0y5x$U3PiUJtXM0h~!HOP+b^U@;>**F}^-j@%v^UxQyywWeIb+;-R0L+W3HTgRlt z-$gOc2FfnxDbCDHJ%Gfk7i=fCEvx9$i{yE&aBz0T5;UZbNZs@(Hu<}7@n|w)k+m{3{W)u9pr$AB)$S85@Gk;NdY5^eeX68 zS3|XNF(_K4aT~J?=gvA)!$+^>+@3qy%TSfN;d-0Hjg+YgVUCRLui!vnchkndD3W(xs|#IT>fr|Tc2u1%NIVWbti$k z$LZ`NkoogKTRFJ|;U-YM&I_^7f-Ep1cy4@{@7a{Yz?{HW&baU+r!U!@C$wo_*!ZQ; zd6edam4=ykmFr&lAUcN9t!^y|6r{LM&Nx_zeUPFEDPI}5^THc zAL@QsK_g*9IK>mAZ8x4YXCuT;c>C_QMfdQnAvXZv$N@h^B}19&dgP`!#4qPxj&`(QCydHlg1T!MteLkCC( zceBsd*cOe3hkJZ;U)%}~Kc3zMX3fdw+Tdsf4+q%?4X2)E+ zf!cQr;pv&ec;Fyg)JUpauvT^aLhV^z9 z*9zO(RhvB!lr^{(o?n zv?x4$|8*r&=Y;j69Yvu+?HRtr!?j6z!F0>RTCclLl>~*Me z(ay7nP@Dh(WO>9Fje`j~JaGNbbzI7ILf)os{O(Lm0$&7=c^jhC_2OxNnwgPq!#s^9 z(t~8U-#|U)4L|#>K783IBvC%fU|7hsBkVQ0+35dRA>LRU)UW(sD4 zc%XCVp#I+JWc$fT2152#_MBqwiBB8Pf*0j^_v(1!Y%dqb@*|ES3pxCTjhF5Pqz$4Q z=*$Sf8udr$*fc&ZN ze2>0KA3tcy2>IaOMNHx__nRmhK=h--lZ2-zipWb&s+AA86U9cB8(cy^TeW zx5w?T#;+X7{N|`Nd0Am zsUvyycYm^=T{hd}HpPb0;_$5_!vB)pG;qw%RoG+T9sb7PkvyWwEo>tJKlNv;aaz#I#$Dj@Qh5>E;)!t}7lrY^BEq~0E<9zIEXrNg??{}$rL z7_14&Lu;QDu1cSp@ZL1STRgKdAJ+K^NA0~U&D+?kWorg9+9gH>@;gx!UHxapfq_^1mB8WPl?y;L%?JmDa( ziL2;rl^YUtP~O6@KcFy;f?kN`#ljQh21ptkOlX{}($e}UC1QbD4mL>=bTq?ch8_vD`)xLG(-E?FuQJBdd!F_GV6q#8gQsZziH%6X%fbv{XyrBd#-_Vj zT{mipwNru|7re9#HNKfH*!vEd&?(5)pog3llEZNGeGGVfKimL8LtAv)yX-}jC_@~Z ztj#emqR+AtT%;yoHGOt{H@Fq?2z34)VTjcuEeNJ@{Ir$IqBwMx#7&2gFa?TKFl{6i zv#|(6hKEh$1)+&1Q6o(b$dosO*whIr#^UmpNcyE_+jw{W{b6N5fRqd+Lr%RV>!R9Q zmX;L}vCe8)2o>cd7Zx{K6b9R{E^vwX_TEkUlQHG&UV`NgeRdU;v#wy=O9|wX_KMNi za-B6h;VnDjKpc6qz@nKe-57yp91?1VY`m0Dp0t7|BY;V$)M3Md?GwS)b)EQ0YJEfqw1d-} zO)8N!An$Oi0*nm4mi&Y)byllalE_(p(P33wP5mZKkhn#oB*IVlM7kK_V-w*ITpZ+Vm_~n~ZmL`{9Y~2gBXxmch7~NU%8Brh$ zKlh2Zh`_+JvHLEXT)X;wO5Vi}<%|l~5-4Ir;1iR3$Dl?@$LQrG;Bk^SRuCnl%mR*C z9+}Rj;-A8GksX||MyAKVI2MSK9NZ&T5N$0qF%o8Nz6Y~Fsd<$PYLqYh&pdWXS|k{_DC$7+X#QQcJc>IXoAh7((Yx-LSlM>tV$Up3ReIa_BGagVO^W;nhVJ9*6I@ zRDo|uZe@}M`j|>&`Lwwz_3OkpMfVMI(&L)iN^M1}9Kdyo5!Ca{Z_ZEPP|zuz7aCH5%}`2* zU#=x8BV;9cy57L8mK5bR5$n(Ci-WOoRde{owlj-rjUy=ch)VL^-`J+w?zE|M(`$}Y zDjV6d?obqvohwvrMs4x4EnUv$#AKG6KPKrb77cF)X&I6$d<#%ho03$gK4KKBsgmGq zOu71HhE@aq1Q^Y?ST4e-sPja>v1)S=ysniD80Mq=gB8H&m^0a(2M4K;M$aZE=Ci6? z)FYfCK%zzWy|)vtcosa8NX~(%7t0|qs2ZSLO!>!)IR+L?3RNr0g-JKgFdZ>IgeIag zeRpXwq|^Jm`i+$j)%7xXQ)^i{c}X<5z|v*RGcm=uvqo8_GDvex7Saq!rui zRA0q|&0Eh^Oxg2>!L5Hd(iicGY z7j20^DJmYKgtX{=MENhw|6Ab0f80Kcg#rX5BKcnepa1!6|G!(N|7kuov$He%$@z6P zv16fgGH^BdAE7M9jut=v|It`w58J$i!9#fL`P%mvUaUoM1%J zQ1+dnB`e4J-yz5!k#Hjz1y_BFS2iWP|!5J~AI+6lF!t(-^zW;ies z=g-$m_+jET5td#b8|4tByZsooc}W+TNL<#^`j?mtWmX7sa~MuEWVjwLuT}T~Rq$Gf zvy+mv)xW3=N5Axzj;J4dTq2xE#>EH8g99-?O`CBv%ICqs%j$gMX-TcQ2enOY!huA_ zI@DmfNkm3C)LPzGE7te>vbQr%iWTEjHYx1(y7#jY zkAZ@eFXtWa{Ln#?*&GgfMrvMm-5ZTm8gi5Nvy>XC1I&!v%~$S?^%Xg3;7Zev3_eaZV?hBt>#>{Rjb} z6hptGjF7G)++)&mfzG2Kt1X^GZn>9gSz+yGcdd-T#Y&REI-hkd2IvE9b%O6+6b}in zZlyAuZ08E_$Ou6Xdy%2f=&>3?4-5-1_u(G4ou}n@c6J-$mJ!)aAd!mcMh9v`j2!~C zMNv^fbG%8P8G5FHDB}Z8D=t$9CZ+>*5IV@z(Jmzfv5G1d74}mIV%O_Z@#Bu0!$bQ4~0U8KEeSE zIdAA7TV>4WM(wm*>qaAfaRa4L_#a)*fnJP`&s8#HAHLFvvQMF=eKna3>Ii%ON>C~; z6EIm`NZHjWi>^Ux_`x(lE*R_>8RInGa>wy1jYdO6j6}(Zkn7L{dqf5y%IBZsRD%hSgI`|I zz*8kTbsHER<1k`b&F!{8(Jcev{EK=dPVb0U7f)DYxso_QYLHXJ1Hz&N;_Bkz8aq83 zKgmqG`1<1C(&lQp^%PLZP^k-u%Cj?dv02D%?O$r)xF{2lMbqid74*)GgKYeSUrYdL zV6rh|%cgO*@fkN)1yC1A|0H2);AkZw;t7Q;V$CpSMzmtXFv+3h31ELwM?nCIifNd! zrIP5Mi6nqzq>U(Lo@5c_QK62drzpKdyEWyoOGjA|d_w?B7)Ol_PuXCd^(B^ zxEt~ftvvcYGtq!3>KNIB@+UrwPz>-3Ui|9ShSb$GVM|8$K|o2x%X?jlZiPm`Xk^%7 z+Dd~@h;F6HE>Qv9#$CEyM#(9M&;2g$zOAbfp4Vcwri;X+iQ56B^QuiQDXhW6)5XQ5 z1FkMX^ZF(MgDi;<;jt)DxLwlZ@G8R$4b24D6pjyHnb43u18%XPMhT)(?4>3tH9uJC za_hBtjTbco1ARu^<^~?eHq-q-@5d+QbGtw*w=nJN_3J19p}P@Y-0n#T(d>6vAL(Q0 zI5>q#7Rf{3SR*tiIzPVskG+e7u5}h9bSZvXE$HI*9!@ysjNvtDQsmPf;X85?38Vhv z4m2$o1Kas!bxg?3>DAMZCg__lL4JP4SV9qxM#UNTZI4aVTA&H}3gKS$NZQJlvURdh))ARgv%~al^yIQMf7t~W;Hzg# zw+eoFQic?Q0qO|VC#>=4?pLgr{r(wg?+GjRY4Y08T^icxSUg0npSoUdR?z*C?4gjV z31=;tN;0;G)|qpy>@dz4Xi{#B0Wf8DSf~FaD*ZawN+>?T31Om8S_cH$n@)OKqHS}m zn~3i4Og32mWiKXh#dfMEe38XV2L@*`R-0%oL92wP*{og8m}cckqm%;_MO!L>@*e`m zOmk=Cq%VV?&-sQV4%H?QE4Mnaft{+n%QuWKky7bzHg%(yR4md?R|b9@?)$E-pPsVM zgL+pvjLV=4O!#p5*oLX?$O!$EfB!Z9HJnnLc*=k^^kOqSdn%1P4A-G+4_>2&8<{T~ zP-ouIIqgU1=%lZ1C1WUQs7U^s6hD(&o_0bln*xUSR>5({THd%KIrNfoJb*MGEX5fG zd&x-HEE8I(iE*IeQ7zIStOT2^#j_o1JoejxU3%1bSKJ1h3T}w5Y>Xtq*a<)Pj#GC@ zSr$)NTw6;Y^S!Z2PjP^_>Le@l!K6)o5k)c4<4(emg3^XeBVDOf-b{=g zZMY?XEK@r|6v5RMAQ~0DiX;WebWJFS{xrg^yHFmM#E3Um!-@8Rp;WHKvI^oeep1=P zK}|l>vDdKUz-8X)sxjf6#f6(?<)m61)U>VRSlvLeyv$v%YShg{`+-HCWcVqX)Mw}<&L_B}%oT|#I%-kppu*#6lYW)2ytQsGV1i*(N{!0WrO*&9jo^6@-M+H{m zj%w`gsS8Eho#Nz54$Q0eo*73G0rq*;+1_N!tY$@-&#-ito=5pc(>x?L#D<1=1&*!< z2+kW85nMOvg*~DwY#w1!V(KyBK?JdidmZf*!bq99~#nM!c z#YW77B!e)a5UHq6-z1VIn=r*%jjND&M|WcP3AX5|6y%Cigi3)47uYOc*$Yb&Ra%tP zvXHnsvI5t7NrR=YIpZ)x&e$;T4ak+me0RqB?H^dPEC@E30qW0qbITNB{-KMR!MK$M zen`!uz!d{8+igbv65-@Etf4Y0=zi+woxzlgxJHSdT0V{+o-jnyQZhwXvg5f>5wvau-Q=CiFO!94p<)QZ>x)3=~g{sKxM3b44Sx`#(>Q8P#n<(~o#kKyl zf(|#Ts*gY~=p^J16(YA3W&vKdJ47gr9vFF#kd(co3Jl(goex6Ict{H;yVR_`U6M>8 z*S;wAyHO=??mY9Ev7A|wdf$c0AozlfOT>HF5vsvq2Rwngnh|#eai@pvGhDp+)N=~h z?5J=tCeTrf#mt68`9P+=a>_8jYom=+c;nztpK5jpy)@cOchPZkjosa*)BEz>Mj3#z zPCKx=2doM@BOOtHNrb=jlbg)CFyO z$(WfZ*-FvPKTq6pfzCO-b1bQ9zgH!9HL>;0oVhpE`iT+$hP(HSm(!)bRAU%Cl6(i& zay6BEH5Q-y-(M=Sm8tjdGCDgCJ~^!8<5tzNR^!u~{)(Y= zC5gN6aDVMG(VKYh!n0v%O)O@-MIE<#uW2Hk@Gf(wIn#Wb@_1v0}6@ z-gLu3hVSbZ>**MxDT$Q*tkuY7nrM-n}-A z8Z&e8n_irlb57KoG0be@`C?xeat2tj*~J8{3EG&3%ae<3dT)&|IF*X_5pK?$CH;xI zy`4^aP=2U%_d6koP=DzMyq!Je(8M9!p8D~QF3>ohH|ug-pgov)>9q_k98x_D8?kR_ zQYRrHo;4tb=~K~JgmLDx_rq3|SiHdp=`%e$E?nmRdBu5ARd0$PT^iHy=?zR$+@RFQ zbSGsl=K_e|>drPdW9iw3j@uW_h4%9%+=dq+SHAn3X_hvr+EA_-qeN8zu_2KAEZET2 zaeD4-xZnz>CUhb5b+&kBnmzYEq8gg7I#U1c>}s>t%f`=c%wwFUBvQJ)+GiLy%?j71 zE;`%4Uns2iXjYXdM)yE-N|e)`mII8p0l$~A=Rnn7#qf`*_Rh7m#FQAl5161g0}Bk$ z1EHX_gZ+a7atNFuk@XZHug*K`3wft`e%-oQA`5;#^&iky-Fy}5pk;I+a8phemSLaI z{7e|A>FG}&rg2FF56#(4nzA@nw9$pW?Iiy3(o`O&?zkwsXPC>Wl$x7QK7}@r5mPGC zXZt87H)?w1564YV*-fOT-7tBF?L9^+Lbfo=CB>%3NaMsx!35o+@OdQ23C%1#g+|R0 z$NXtQB1uPWSs~>n{<;N|gcIjOb18Y}6WKN#!dS=7jlxC{7ZlS!H$We#^%I_^Z0Wk_ zw#4z2UP|W-aTDe38}O^VXmJx#71ReKCu$+lkZMoCpp}8^nLsc8&^3i|H+zg}l~OG{ z>DSE$tvSF9%kq;e0+&Uve}jwz(#`Yd19EZrX%_O<(j;dYr;uk5I}FVw4a-CEp>qeR z!Ai}@EC@)6xuS#&lE$LlJb2MU#JK3z#n!l~0PBmIT1=s+XrysyOF7Zv<(S0|5nX55 z*?Bt72y*T3EE`AGG-T{F%J6Y*kzA)DA;Ppbp={zb{CPSq)3t3UldFTHLT=r|)5-<# zpisForuSEtu*C}YVru&Cl7&T7MA}ZB@v%?a2&g3{dd0L&jnC1x^kUdaMZ6}xvp+aG zv`jcoN`V@4kT_`)sA6JhU{Z3K;G>KP@gnN+WfaF{KRtI<+afs~N)GCYZt(2xuR=4d zHqgw#iLsAEZht+$j6!GYaNe~>rpB^39R_1gd#pjATW`ydTO<*Zr7_H^)@H#-}38LPS$F2SDUl_9#eEzowL{G&b(FBXJY%s&gh*|zP`e#|>t~&F*K$HW)*O_uBp7`z0kOf2Nek|dlm9Hr)DJk-1xWSdB zhVvTv&dI78_vA-1d?)~y+fQ)vNVm7+xIy(7ZDZsd#2VtvAjK-^ho)t={o5B^-K$~J zBM!VLW-J}8n)BaRddvx>sN5H1ZbNmX0GTi102G&6y``=pV^+@s!)OkSOV`&kYUNpI zY-oCj$SrBgcn+|1aT7y@gyd5!YW-?|jQhVZi@JsM@Zz_!Q0@QtIj^<97ryL$7P`#O z_3J4WVwDQ&m$@hK(BGF}M}s4`W)U@Hs6_#49V+Ko$Ac1lBHgV8AVkdR{Q08|#Cz(Z z=jw?m|Ip{&aNpoaiIP&GMHl@pFyMlhk~dAq17?g-y@32H{F)e}};cftImUcu>GPWBM?GX4oB?`zBI5DVbsP zxYknmrA5zF@4MTO;rqxJ5LE`*rHQp~=eY1kgl>&{Kr zjxgcT?bxv^>;BL(Tjlc$=486I#7-;8+?Y_+!OT*1lr92Ab@${ZssG+{smFg>(8(ll zT1o97#7-5nsuXMR!^I-%y7{{K8{z-Oo~oU=yxd8C{hBKL|6xy#2G(@{Hv;8kVq$Dy z`(qFMAEe2}-rCOK$G69h&e_7s+0N=mJeq~&zpZ$DbewP{5?5FrX~uR!(EUrmKRv>1<)?D12zX$vRv24OP00DAVH!(3a`Oi`7em&WeBX)%!ol=$2S%-J^B-mUZU4gt= z#fTT`l_HcFHcmvPB})<&q#spPf*-~5*T;hBV-~7#$Yc>3%g&}qJN}s&f3Vk1RYHyo zA8;m-;hX&l`bJd=y zSLu^b76`JUMLxT_=N=7C5!HFVT~~@G_veTpgNlC|Lyqce7@{nnv*pwyGkZ5Xee=I+ zIjZ%gr-v0Cp7*n2N!PF2J6;p7_vYG}6?ub&ENEmBEHbpou!v9~Pc&MbWCU`jBF)W2 zi(t1R9Sfa=^ucp5PHqKSlvr`L9A+MGBwMM4S($mHKml9@%&54yySV1P@zW<_n8)oed)TT3UvScYGKvN3LrpJ>Uw~Q3IUUoa&FjCB#(lGCy=;QShD0=z zM7WWHtH(|C)H-l%@cPz=hzeVwY{|C zTEnzt2JDVBRIl;gAyPGHwOP<aIhRDa>iJ^tUl}YlaCwqwj=Zj zlIX?$Bxzz`R_^B5u?}$L+{^lf-5C+^ucyO)gwvIjvc~-*%oJu^@)}aPfs!jRX*4ha z?@0R#XuO57Ru0&!sS-L zI5$Mn^}ZR$r>~5o^y)S+;)oC7XrMH!j-Q%tAUC|={Ah@S3l6PfSTG{kqBXk`HTHa6 z=J32`Au7#d0M*z^C6q;yxCVgI!75~Zed%+o6-p+JAhfN68af(7CGY@s1)Ukl;&05Z z6c5*-Fh;|;ZnHmiiP1S}7+6GhHv+=+uK4v#!W}DIq!>JLiBPBqsN%1nq#oeKq!bP^ zUHJ8=BLv?!dfzauej(i$gdSiAU*Tu~a0_IR);uSD&3j>B9AWNI;ly_fejrByBT2~C zJj!v}X|Ps=?YMc|JPVQQE@Kq)Oe*uBNHk)D4AN&0Xj)~2!$G-PA^g_O!R!X$J!;v~ zVD(~BvT<-K$V4H)9x(w@!btruDj`HM2mo_p91#ifZlgZWd}%?RJ`S5_+z7R_B1;0~ zie#dr#=)Y#WQ_)tSR7;sL}sa_KmINNoJ5U08?|RnG?sgxa&(4-mmv{}rbKj)gjn#7 zSP?~}LPk(XCQbV=>z_ zr-#vECn`PAW=#<{g!eCo%QjY)|JIcNxqN|fU<$0u4TcB5WI#3?kW{PYJ)FvXYpPS< zmBy55=kR?awn_b;H*(r)?ux$!*S$gWNwsdz=AC3`7(EM>9CjmmJDKI`yVCordI^tD zmsL#5Fs3^1o_v!o8Iu!jx}NmMKc${KAN}3zKkao{fOHGSgy=J#507vxBCCH3TRvWS zqxdDuZg*%D91hAXJa?9gCOUxqqysSmx72#A3GIQtSOpe1Qj_{v*IS)zu!4iF%&ci* zGKPWBRP{H%2^j%>%%J8IR!2PH)V*!cn~%sXG;n~8#9k9s299|^YDiZLX$jI=UQq5~ z-4+ZWN9@r_p~>;zv+znJ*@qM2KZliP;gjspcP>e7mK`r5dO7CQuBEdlTh>Fm2;HC& zHPQ3D+RRDAfuVg;9Nx(gY(>Fr;~T7^*TtzSz-SnN-F?M&o*_Yz4pJ?Z5&P#C+|QdO zsF*81Ol`b82=gi6{p^+r9nz@Ww^eRHvg^K3bCu@#Qy zcri&eFlBE3%`}&=OB(hx2Kn1+&zj;UTdB@(S7RCl_PomuvS;mGB6!15<_956a5*C; z*D?&ZLPT;#nk;tyu{WjTc_y4{g*IUxEd&HA7c(bsKcGpY1Td-d-OhDfiQ2&; zfnsv3@{8&ZfV=HWDlFT}p7~oSp))77)uA{YwWcC*!hoJ3_D-baVVoz$>UQ<-_?uwj zoGjkeGjrze0qYFEQIdF&qB!>h67Hpufxz8NGVLDDY7o*Wr_&eC@Q1zm^&a{ zWtt6&nlrE}$WUB7$Rh5>vsNwcyj&@%A-uS{&N~yb;x!Bpwbs)ZvYgO}(s0Lwo2(v` zyo8V0IB~#(3AkV&wAjLTk%YQU1Y9h(fH)h0>e&7*a{>>;nwxz)-{MQ$wz=7ksP+5FlcR_kfY0T|qLO_g;pZ4f( zjb+<&x$k4DAhi%&8?aK%O~*=az{9=Y^A6Bx1*;B7`}Q@1gH+ryyavSFjRcU1sxh0t zJOlG1XGo%XHLsDWrc8T>T)l%Te_#9xNMbT$BBqbf;rnUit43exWv9A4DzY9*f-pN} zSfuBl|LEwe|HB>GFC2;5jLI=<4V9-ih^#~~60ahq%}PWikAGrtW$k1upyPXFBw=47 zNrd>$z34g>r$De8u|pf)vRo8$Ef_MzN(=KyflfRm3-Fv=D2jt&otd}ju6s~uh?j?a zkRT+(p~O;vT)NM%`*2ncb~?Oc2Sy+4LuL)1Nat?;Yu*!}e51YXGOKVC9dh`|CN0$@ zcyrU?lrG}hZHf|a^s6BDlAZpBCN;T|7RAUnL{Ery;(pu+587UvV=oo}!T?{^zn?Dd zfT_sn-QQ@$0-b#%YM5eLRxcXzEe=piA;fVDi@B4^DdfUY**|OWLl@t;2gV*M5W$oG z$9gA|2HqFhij*ZZ@)P!QK!@(YO{;(A5ZGBPHB$GLUuu9`6ZO>eJETd}#(1|q{NoRH zdx3B$J}XU2-e`LvAW+`*wf!X*co;Y_!xeH1{ngNC~qv(lhxN=U?%NMPo~|v65LuG$wIm;LPPBo;PsV< zfwGwO`D3iFc!>9M>Cjn(Oi%gDUbj8dPXsCmLQ zr8FHGv6)!iP6mi4i(>L|k0Q5@Da?*(f(wouRkUO`QNqe%b0z{s&PFxwKoZTLjtB^* zrz~fE^AMUla_(Uil?28IRUn?5WJ~scH!aqzW~GhA&WA}M{-Gm(KbXwwUinnzLHWH& z|3Y@B?&Y&l`IaLB;~rq%48;k`PUX@zOtr0 zO2-PQX1$w{&*<_C%^%C(PI(A8j$1#BHQF$1&G&?f0-1l{<9MUHkMItTV{c)&k&WWX zdhDNx0uAVTq~;D!jYf-~Pprd2rUy;uQx;X#%^LK1FB{vEP=Unxg~?;mGT=D=rk}r= z60HwScSl5!uD`ClA5I*E$8>?2kFlzU)zEHs*DG;ENh^p!E>E(vN`+h`zZ`LUj zn%mhv-Kd)J;;~Nm^4IORGz=a9UIp{IHr%2SH~6QvZ0|<9box-Z14cfu6um1qk40yh zneykHyV!zn^i2}dNMg*;%GwE?&GdoIHMsvIoBw1rk(L#q77Y=Hzn;s+tvcqyD;bVi zrA-F6N1b9iSbJMBmGa-pu=~F&?%7M72yN_h-AsI!(9`fB$}>HWST4fzg-B5|K5lU1 z;1qXzgYzLvC$nuwsEirVvU``-_(r-~)2qCp1ZzT|M2{l#`Eur_oxa(q^&6Y>=!*5}I~;k{6nC(u*GGv|N66=EbXW2jGH&Ovje6enn@^{Ke&hgG zExE(6gZ4|v_Va^wy3+e;E-E|exo_U2$FE?3$9Xu-UFEVSTV$qZA=g~0?uIvi2IuG; zoqHW}x1Ojc@m~SWKtr}6+%g2H$ZIa4Sx2~ScI~J;M)-0R<|uF?gmOHXBOZqlB3;-| zmvcu9r+Hf?ODS2JA6-ma<0_|LTaj#nc?qU;zF&i<#M~nmt~8?Wr>HX+9xD@ ztS={4v>3W^(Ds!6?%MYvOZ`npP^B(=IU@!2t#t0Z5+2_J-@xhTy3$(i!StVGz6o@_MBJ z*VdlRysvkG&+hN{=L3~q9dDjfY$nF1BlcLo? zY-wr)Ci{K~pPy<=v9Y7WMOmCe+`mDD76+}<@gwdFcxn2WR`|^ks!{E60>1nG$lu?& zGv@~(0w!|266M3Kg&&L)84LeFu1EkmA+B71gl36+3`6;nY5r_>`G11!H1f!+DB z4JZL+xix36UiIAq$L8#b#Re)G&KgIglMtY8=;h{>@}g*%!vE>`w!ct3Rzm`H>=R9a z$sFmep4+DHX9IBe(W_h`JqY0^YJ=~Wx9_$;CQr}(ztNg1u0tv0zmH(9d>rof0FQ?^ zk9HRav-bf0R%ikOLSwe(WeS1MeZL;6?d{s)8UylIP^gHc04d#+wyI2r*jq|A9T>|( zj(KE0V6y8{TzM=>bmOLxt7{G;yoJTpX-A;Ec zVx_|y)RsHrQ<0~Q)oG9@!b>pwD1{RWz{H9e>=&+TqOv71#{8oYAl}~&ogB_1)_-X8 z5EqCO_2FujK%ArvLI)MA;XRyuU}^@`j-H;)=&~Ze4r9y#eQFot-$A@0&H6}=mBUHa zH)$)v^0-I2F@j)WOc1qFXbAR-W0t&AQFr!!c^%$~#%YAHJkXzKXBBxBrGOH2{Y1R* zz#%~IK`%ZRA%hj`ar=T+=GpDJL}?yD9)3F$>D6DdktQtU4_gjIK;-*VM7^dk{>NBhOeK8)kj#C1mJ2){LWHXCv)=e^QiO7)4c^Xb(Ju8)1jQwU zU?UE>078<9tXQWB9`ZxR1W|U62g{47oXs$E=gWl6(#L!+NL$#zqERx#ZmqvE2PD8K zlqG?(5mP*y{n4pW7x%bM6^ER&*i{mVNI3awAl0Ttz)qafuz3(uVdmY@C_NuX*VJms z0<_H|`BEBhr(fT56x<)|2ppWRwBF)FQO6jH>^$AIM z9cP}&ofJVvGFO0%p^L9BDaZmYfUDZgJc1iJszRf}zbif;`JXBuc<&hOg>DaN01fjX zRK~rJdiKg_IjzBi%CRrCosr)+tNgBlRo?ayngKMQiY2fy^T-4Edd2GJCof20{g4T> z;_04-e_R^`+VvN|5Q$Fy?2sf`LH<6m#A1a05L7K$=~O7*>4Z;8M{HMqPqL?q4s)+G zxOt@0hq@WXG$ajlC^@8K2^uS*J5v}bQdTd&wwZWlyw##4(u`x>vTa@qMtpI`e%pcwUD40e6&XJW*Oe`OXQ(>91%}*7xU1ROfsXF zUQ*E`qDd&S^f;3R?uq(P&P#q;BRhK%0GxEq3MVnZ%f|&Zq@*aJQ;qo&>%&FkZ+GPd zzQ%QHXL$;#;IeBYP8Y5l(MVj%6-!4XMU>Wkxnu(_&O8Zq)wi4(ZsIL`#UxtQ&i{}~ zi%aDZf)27Htd<}TsExL^t4*o)PHlV=m?xL*;iPDHYaNk-oBSjuOWu@QF%{K9{8e?5 zN*+>f5rSfMYwFpaA(Pw5J369mTI&2HjN>2RHECL2?&l*<+FY!VZH?UazK6s2(lN%d zO;S8l-V?5@y`w=f?pph^s6_mnvuw%LVGH*3<3wD1YL)!C+_W;?X)W5}^Y4xwldYb6 zrP5BmF3nb6vqDDMhg_p82v>Pc-py7tC%Vl=vFd2IVq6=w?AEJuwixcfA}^mSaS30g zp%OlW_f!SewXnzv8M03myp+ycdLUMsWJhNE3l&RPAqk9dqX)gfs+xsUodUzo%IPe7 z{v0z0p-Zrd1K4o7U7?{`kAXj()PpQ#q=VsJ42z+tNy}2^WMWlJM4XJbLFJXYj7RHm zURjH2k=wEtM`cfq*#m&gWd_8=EZh=m$yVqExD+Gtsam*;J@n}RkZKNXpm#Gq%cb{8 zQz{?S)pHqO&2;Fqn#W)=Sp&aLFkr@1s!q%bDO!l`yQ`R~IfAC~&a{O2sH*9t^`}5i zy_Q$%$t_GQsqidU`~F>>ToKATQO*#E#iMHdsrwQW3h-; zj#Jn;roKGU6jD0YN3~e%Ap~wg+=eT>6Ei(WF+HP97$GTyMUP3d&^#)_9vHa$#Gd$@ zI-W=&%gDuqdQ7{PTxQd3k>nK=`Zs)l<=WpTrE8ElEP|!1P+}2PzrBMZs>yZGucHvZ4f>t%vT=u;N5CX&%G3)62DEGDEY-f+fDqKUoLViRw#0+ zV^f~4$#=N^ZTSrxPD>A|c)jtmi5=$QR-dBe0Hv>oeSy(6*#p(p9v2CzP}z zyezj$j)ZZIwosbGzX|EL_S7h`YLxB4mL0+1kurg0@%8!{GBh=|!BLS#v`9ce87i?6 z>=H8oi?eVqQn^CnTGHQq%9MhXfT($DBvv^_*786sy`?KLo{}f$5NjC&S$sn!e;MNl zq!Rqy^qz{Me9(>on;GYab^S z3@#~}ImxCraG$}Cg=Sl=O$L_Xlw;0^r#SULzki~UEXiyAecZ;-^g$7BiRp6FKaJ>o z82E`QdcND#+!ty2z+#HsIh1(~-^S>s*63!7>DhOu^@%?A&6#8T7!leS*kB2eqYPH# zSR*dP_s}K{(%H3%9AV9Rnk=uj7W%kDk*nsvExqBf2TleC{$bHoL+PS(eI=P5 zt+!BS8B1YxsI6zYcJ_(PuWKKy&^3LWz0B^TIDQ@Om&PI3>HLluf4S7UYdJV)h?^Qs zlbFsE@O2fB%_Gt-6j%7`LUkx}zpR6w$DhkaI&5}uF`jh4PBO1ZOmN29BHi%hjws$` zF|!iG+hP4H3Mg1&60yfA!zA;|&=-*7H>+#>xtf%8|K~nQERSoB!W6H&)pxGeuZD)X zU+GlVO=mY`mEnx`!xiW3sM2Nb3}(gyv_c<!)z1NK0dQ1!}Df&mx36FS4c&uadT_9(8IZAL~LJLMNcF;|VG+CUKb_LGhEa)kZb ztg)uI$od}pk0*;~-+GLD9+80;GULJmg&Z@vsvvTr9euiN^3CfX3DvMX<7dTLioE^S z)2+U*?D2Xk2TNcsE#sH_K^l*C5Z>xTohcaaoRMvtxUf_O>Ar|S_t@Ks_G$qQj)i;T z3`YJ%B;g$792st`K&!SKss=2l#Jy8Zv{Qd4qIRl??wiFktpo${5{ZCyeiz#Ll7!3k z1s0z!B{eYQF5aE<&R7>e!3N{d;wQXoEeuQgcwOb_)y}$D{wyH zmpqDyt%gO`KLZ}Z&bR$mA64IgUy}9)!?-KGp11sR(2VFc^qJ0H3c~Wr?y2^XO6x8I z8R-m$q@PU%%AKmN2jUv)mMnP$=yUpyV|lse9Xi@?4wm{X7J7boIq`{qkffu6{p^oy zFDJ0{Me7Ffuq7r+V+kNwA3DRqquYsIcJ7NG-lws`D7*iJ2g4KCl8xTt^1G3Zc6m`S zMKsxC1Jr;-0Q^BABAuZX$JW2J5)IRH4ZKSB!9uax+{6|GstQ+wtfFVdWwIDg!%V#1 zF{7v9w%vTNA-4--v7oTULWRIs48i;02{p8vownn<{z&e{^nbZEu-9?E$PKvD<%_F{ zO}$s3o(282br3cQu+elC1zC3~Hwpk`oc?2tHDrJU-gZnA5a3Z!&3B`$$cls7Mtis_ zbW&HeqhY)vJ?n&@g23vM<{1rLL6cFKb_8`1rd4*{_kYVJfTF)x(P?wNYG?%Z;OZ0! z@}t3uZ9CaWfb|OXpony*%U-%vr!oZOmh(=+j`@-@l=|p&eF|0C(Jg5Y1nh$WnV{#| zYX}L7I2)6sdFfgQ?B z&A@>OjB64#LtUnq1m&SX)ABIt3_tNs>q(F2eXc?2YzNISS&-7kaVP96gNNqB+b{ps z5ec@wwemk!2WSclxqf?|diiNt!)TFhCu0}4yX02y*k&KQ*#A~%J|zDX5d-RrZP*?Zt zCfuKv*@I_}jwXYbCyk>|I6GTRU-V=zzx{Qbp**bnYr7BqqysnD!soDn2B4rps~)>D zL=MsxyV3+TU-K*RkeNGJfP3#SAN?R{`7@~Nd-zPL(at+J%JO?ZVkE}JooomXy$SCHGP>MNU&+g2V8ZUJ{gR2{EdKAqQQHW7EG1WiJjWVKmY zM7c{_2W)OjO&Ex-x4OW7JOE9n=ZP3jzaRv!?F8@I@~_U##Ez-w$d>;1O#+QJq@(MP zyO~sar`(^>oQfVx)H0m+;G8IyNt1KB4%V=jUk$@rRt@*>cw4X+S8Lr`vNl_mi;G}!9#RQF+B_)jcxkRaI({6)-;jnr8BzQuBF`8kR#t=atEHy`iG|Hc7=`M zj5do3lac#XS$bpjO>P&jM<1Q>gQGSrEY&z@^t5}+8$~A@vft`*+Z{0PXW-NhFeuWM z&wuuRCs}Phx3#!EKBU&SbS8FRb!f+AIQ_>f=I7#n`^VEUu`)CLFJjps&$_KvtLyZrdR0OxiB^I!Ak{{**(ug&IKs^F zc13Q@0(D@4oXnwK>?(evOC~meKl`abtn_4}CXmGEPQq=u5MV@sTNHI>owt8l6JTMIkS8xchtJa;_SY ziIg|-Kd>pL4UcQxq>->02GCFqqMKCNPL|*y?2YW$CLGNA9RiFY>{Ye3uLXk8{h+aE z=;%9X2;1Kn7oJzyoL5D#C*dI>V4TUvGEC5$LwMqA=Q#v{} z4v-S1ikbfs?G}LB_DC{1-eID>dxUb&4G3xR)tkB#F9UN$Vn2JWIN*APC{*eM&7mi zfa?2|l}CvFp)-+qn9Xc}Ri57{DSSDwJ9yHwOFW6`m`&s$O$Nh`w2H%8q5P8f9jZ(d zx^a(%ayvpJ745eaoz6jw@0!6qscgZi94q$$sw4aki%!7j{d8k~3`N^N9wed>3c=H^ zhnIK3>-o=k*|FM4oX@#s$p(Jd0c8Tvq9|6*!%n^t62ynP6Yv*@Aw?;T)zmr6>%AB` zpNBHYP*D;Xy!3HM%E{$&AtL}x8W<&sQ8_(`Pvc@#LqP>^fU$sIFaqD1OwqagY(%@f z555?Y(lkUwSf`%08_Ge=3$i#i6JX4h4}ZXf!U|+Wdf{Gv)C;BriSFLTCp<64Kx|}y z8%|N5C&)>@n_cX|gAaBKTm6cR|C`4*cjZ<_a;csLa-`7|En{x~Q=^VaM$i8l6whHI z-HCoFO)F)IFvl3R&G5ljFhl5Dg)U#BYb>x&x}@>``LX?LaKU~)9g;)flVWXw-u4KF zCgr#;!tfpm$jql&HB_pSonix&Ik{pc)YOCZgB7Rg_GZ3{qa=hNT7YYAwh=$S5`w;C zUv6RqEOWxi9KvZ;tw-P zl~ue(exFu`7nEBIuX;)n`Xs1lO`ZDT@z$O^xJ5w-_#CnxNu z?hIl~BGCv-$=sy*7hZ!EGTLOIZTW%o<`LRJqcj<(kHfwH!;CCS|poJxA(8*1pAT2KO{gN3>aa z`5&SQJ9NsF3l!=3bu1!Rcoufit~4!-by-5>%dVO;SrQ zim}F+NWLB;CIL4nmZ^(6-kkRB+f1cPUGuPpo}9E=@&|?LMVJL^w(t7WnUjs288CL; z-Y>h`J0X*IGp3hPD%K|b`I|&YKr}o?b=X(#VNg^`V#O*Zf$(K%*RWi5&2sf-!(`t}P*Nn3T4BreE@G{DE z;Tkd2puK!)=d~^DmMI&@f$B6^t?EzzUgUn7fKAsZh)6A;WFZqR0= z(eqp2U-u05{8K){ZM34G@0f*oL_y=Huv*2i#*$c>fL&=~Ok9Lktb-$c5PuqRx+7tn z=*SE|57pe*`vI8Ip#|JB#lWgcvD)hNrG|(|Hsx*;4A6PRs!)O0M%ebIA60$M*f5cj z9Z7IIgFK7=X-UYK-$supv-Ix6)VfBV~$*NKX$S@dV`&hJ2rwja7;c+jLcO`TCrLZpQJ@@C$sa2T^@3bdW)a(*4 zPlu0rr)haSmMy^Ah%I*+}N~63udVvwyz4 zU4i~<-RR8HN_n>Rl7WaUO@FSF+U<(GV`CMU9s~{*%fTZ!C5wHm|7&BGBZbxlu zjUb|QY&1C*`WG(G32Cq-Qhw2{2{kKSoR&dk4JwoJ3tbPFuiSjj$i&=0 zJpXUYss0+pO6cBH&}L`YdmO&s>ta&rG<-KdH^0a29mDroR<~T=Z-)jFVN=23PW!UzoRTO$TDv|cnAkALS8+BZ0o@^`4r|7=g+7;NZqTr;)FYeZ; z7@vd0C@C1jgY_i0iAal~h2dc`xB1~NwTwaRs3KHvgUsmZt;={0P09Udk|Gp9>uQ3v zkzB)y-f-RTc&HQ?o#`VNr03?uG)YQqmmK;;`ilbn!wVoheqNx@Pwc3A__*l199Ims zReVfF3rH)Y7TXI9RBuCPlo9Ttgs`ca=D~HQ7Wd3O@HlF>CzTYEn}goo@Z~V)2-xok z_*>Z6v7sAyobJ~@~179CueuLs)3D-kyMYPL5Qy=2Mpw5bQyDp+42xfST zJxl){Q3U8I2_JDg5^Ww;Voh#^m@p4KMnyxBwPq(RXP-L00 zWGX6{TO{S%v*v_#`lkJAZ18ApB+N-K7v+lV1jHyX(E#>b}#Vr~` zjUOMsVdkn12sdwJ)6<)>v43ll2OMDgDE&3oAxtskP6WpJ&9!*Ae<{2CT@`x;C!$>u z9e?V=^PFyJR^t9NQ_80-&{^}MdB1Qa9+~FSg|F26E5Dpi{dI1#FtMqWQm@dK zYE9;SBbesHX%R^sbNZ-=s38f_i<2`oXoBk=Yh28i$MxU8Y%&U@OVv4bG+y^;bB4v5 zoN)Q`o`ss}-!mx0XP3{{yW{)E(-mvrI$Q19AGn{t*=p6L?Ojq(+cW46BR4thR8ExT z^U~2n`Kk6;N5^%patQGT9sR#r7!ymCum)Bl9w6v794d;;=@6w_tpa3Fw=(GB3de?6 zRJat6rV1W-xbidhW$xnQEu{4=VnUIjZ_t zJz~wTL!b?Ki0aQes!Z9>aYpm0xebAykbUCTN|m5UInqs~7iryuB}p`6we(FMS71f5 zBL1YcLJU?RL5Wgp@yw#D}F_;tc{cmPAh?O(UPVL*A+QgA}2ylU&-B%a$$uW~l^w z`esD7uTU+0L3z?wC57dL#r|OF2v#fq>o!^Vq!S4U?qF!<0h8g$$c%BSR%<64OSpXU z%+e{RR7Mk{jB{z<3a4kk#f6C@SqG7$LdnwcP@Dx3KJ;h@mnGQGXvZYh@O|8aXm>+ERxJut!EYcOJ+ezhM8Hstg-4N21z}je` zS~M42`;EWABEwBgJSf&0E9|&>)Hk;RXL7?{8?lOCK{)>0V7 z#Kmq&!n%Au0%scCBwC4nIds7O(M2OJe%W+FykFlSiSw3pNxSb`ze$JV>1VZCmJQDv z|KJ!3UPNpwiZN4lsEF25NVQj0D~2)pq^_oh478XJ?$YwX6h>#0jQaU5ChgFraD&>u zd{}*Gj?IqX>*C^~;!tt!;Y|4@y733n=}-$_=W3%LBP!bWi1{XA3V~Zsrh!QSF2i8- zHP6m$MNMvPuQx(_UkJ7dqHSdihO9ufn}Y#3?Hf#VR{p3yJGbCz#fm$+j}-_1p1LeX zx20_klgrwReM%`>+(?B2Jcfx$RwPqBQZcLAPQ(7E82Xam2~I)xky8sAVvV`^HH)yw z?sS#_vhm>W_c5%3V{N)Mb#RT?7Md-_uKI2pbZ)1t-MoY=k)l?R2fm5mCpT-5!WckL zV=4gGb^V>bCoNy&B-yg@7-_Nlli16xMA3BeZlaAM>-HXK&3YgfY(Qo=2v%j!_M20y zu?}Hvrq>Cajf{$3G_0~RAfj=`(z%L@kTEHjexlmmj9EXslz&l39FlwB(^Oh83{BOI^dt~i}T1Y4xP zc)FHpaoUx9M6S6iTpJ>gabRX<7(uT;rVvLjDBY3O*n^pcKWBDN+g)uc#@f%hq2Sgyy7+B1DAH_qv4)|1W#t2aDw;3_?QhNVVEc9 z^7i502wUotO6)5dru*LNgExOxi`%Lx!;L{QX_3;VN-faSr+Dtdrb?ZCDM&4(rtw6| zC}a-3*y&2woctsL_p&P*l5SZJo4Fd`SEZViGPycH^~~Cca-H0ydjt4mrW-ZPeDW%1 z4IZP_>?TgHqPM(|;TCLB8*<8I5vL>GESdD8g6u9<-n=&gF>_UuzvD`ros+kNH z@5e9}AM#ma9_Bx{noT$2O)LXuiy{Y3F=gX{Y={O{aU_(7R!@W4Wb!wYke&8W4s)x$ z$M(@7)4ci1-`u$I`aLH|^A6^^8j|G*CCoFrmYC3~F0Ks<4D*L?gdbO{wjYnnaoY>q zom$sElhbj5QEt^GZ;^O5&UxLaovGGvZIk!^x}%4IU>cYxauh1@9#8p}ve=zUuiw}g zzWf-EIqG7Y%lo8fA1<}=W7x$X;MxZ~&Yf}(o;vr+_3Hk0sc%%(Ou5oL?QNH-XZVuc zX0GRQagyl%7gz5XBudmK>-KJQw{6?DZQHhO+qP}nw$0tP&C@gIJ8|#KpNgo8`cV~8 zu`1qNnNJ$;JaISdR4lJvJV_KvS|povcgnBlQI*B%RLfUdKdP{1Sjm9*4-wv72y0q$ zPwGw&9c0dOV9O5_9leVA+l0?tJFf zQ0v!ap4~Rj6hq)*4#xjS(0y=h+d57XS)PHFNSvVJjy zDBVrsMY&+2v-{50QR3f?ti6hJ=7Z(k$ndE-&&F*3$<|?lL%S1E*Zwp)3eP4>(^_Ev z&3|4?a$i&2n3qnk^{DtTOdCY+OeTnA z&W=vD*3|mu)aKT|?a}{{6K(B`ZGJDA+ByB-%hdGL*1z(i6*VI(9rJ%zOBbtZ+iZ#< z_@v0l$Gzyg{!4kKUErgO%U&N!n_9C=0&cX|W9p1F7&T%QWU;0U`LnAJmX=5eY~4~G@~5QHn)*|hSrtQ4(R$QS(*V9@ zzhW~WwGwfY+`^nawk0Ol<%9?=oXchHEoI%fd(p2wM%f(k=GnL zrE)4XtR&EaV_+E2&7ouQVIM$^&0EiCgGKh5VCQbSN+V)@pM-#^)cz1QA}zE5g1QVJ z6^bE~jN9o*zlKYPB3>O=^)VgJtU`seeUPq2)nf=2{Kpy5va!i0hUoH)nGh7c5{Lvv zh}2Obqvw*!gxN}i1;WM=rD%2q$<|tg?NpgJr0mU5T)S>&W4gdLX|#id&v&}iVK z3=@Ib)xGWWZQ7Wl<%SBmdm1Wcgl}5}5K>AJ0@L!Vo*L|c?<>39$srWgVf_-i7FyR_dC z;34i6=f!+Ldm`W$b{C1Uv#2n~Hh6wVMzKXESMx$mAyX+aWfY@OtMa(L{6V~pjrY`k zLl^qWR3FY711O*_6&~th7LCYqeBf3*hnin=J{92(Z(S^jZ;FWt%$1N<<~OfEN!+go zHJ4^wfJf9SWT_5L0BQ2ICM8j`+DYWH6$O*w}r?EO?YRrP%_&9E~ zUVkCF)B@4yCBZcqqhKny^U+1;4aJ>->+``$;nvIeqtVTYIP>>tJy*)M-bZ^4)2#wB zh(j9IHIOlMB_x7!fQ)<+-d&e>YTs2I#P62`kc<@snxbGH!pb%1P@{-%D!7)Krzqc_ zs!^xNR=Qp;BdI*QV7(Sfp?pH8%De|+%#j)Rrb`K@a8Omwo4-jK2Ti=BTsVrLBU>6O z*P_Urc7Qg3SyZg$Wn;o>RvTb}*%1nvh6u%F7@Mg&>>`-CqH#1WBdtfC%w-uS3u7t& zCs=KYPaJDNlGwJ!cQr1u0x&<6#Qo>vhPHQrKLMP?Y^CUG4|zL0=@*(0ywAafG zBK5BK8n**U;^mj{QP29c&R1MkjMV4uvhrPlL^*uN!KjYem&l@2YMMmqSc=5l&C`lW zT8$$pJF)}2kW=B1KyN1f^Em3Jg(mAxO64>@tuc>%@L?3_}jvk z=<&q4?$iIy)62qaHc7z%0Q6J-uRS*tXB!*$|N24xk37AtgFZFGf2Zm7oqvP$cFqP? z=8k6n$<$NR(=pI9{P%zIO2gB3vn}O^^9?EJ8qgYOaemSR7;S4>dQpbX8jI+93+14H zFN`aqJzhLki1-QmejBxjNK+=ODQ5~Wa=3*r@!sCe?Kf`o-QIfwevJixLQ|S^;n~Bu zw(CeVH4oh*8cu9RbYnrDdiWakNySzXu?4|Rs9FKMj~JJQwW={6$w4W^OsEkb-j-gL z8IrQE#x8o8`qkad`||SK0i@oSKy^$$y}*>G&oyC|a7h2O0PfD>uub=;>$k9BEY5W58xOkzqu zcPR~&;4_$D83zZ~XZHw3C_B<%L(1_LKKIIw&0dNB|^6(ui8#@?PK$XAc(E3cBP3Uqxyqc>~`l3 zwoTGAV~Vn4rQPXGtD^S>0IMthAwI}0JIohTL0|u`5B1uSWngF;S8cSH-wdI>NPxv_ zhv%BliZM#QLm$mGZ91ugz46m1ops`iD-SwuKJEk_}kM`&upLPfLsc>!YM{HS6sYFWFSEof0qP}enBrfj6ZAKlRO@~nw*DWyZ1WLEe5YjC9Cj!H3Yef4x_FtF8b zMVl*fFfi()_I z9E|$xSkh8aBSkk6PCO&ww2WCx^2Y?VNOM?C$lj|5kxWV4AP$54k{Rt`k%gU*(EReJdy9& zlWN^V&8nS;fK$ykeaSWx35R+N>q0|YZN`R^ts0vP*p7NY$1g^t=A(Ia7{;<&$YTAg z6{)j^k>#RfETn+i1;Jv;ekVj*%{PsnQoNA)qKyu16_>{?A+|fK%%{z6T&C- zNV73_u|y{H`7LSm4)mA(Ax(QL8Y``VoaTb26-gS%=%YGKrlDQSM1RnW=IyoVom%uYxFc8+;?2X&lFg3~oZ8}W$;xKZgIH8t8PZZ)&We}mS>4|F-p{Ky z%CLYa;=(WwXm5;nt|IlU4n|T<&SN}F%nc=R6g9}xCgh^ez*>MMvjQ5LYsQ2%auZu@_ z!|Sr8x;s+Fn#ln~eQD&vW3-G^cdM(~!Il=9aG|yt!13w8bMY85N9j!(j4worrNk~Q zp_W-3Dy)j7^Sk|h9yem-*q}%&QnQGJd7`hG~*fn${PzM z=>MS9#mVOw;lOyu&QHOjPbUI&6G4dXb;?#U`S?q4Ycl$zwz`iKM!(m5s1>S~Va^{U z&{0~*1QDrXpbqWI)9sAG7+#km6#|%<`AkMR^-aR1X9~OD*STG?($ShXdLf4PR8GI= z&`2JrN;em@TDClAv7e_mymX=IO6QyvOZ|OK-Jc{Omm;S3Etx4$dI-+p7y(o*-`{um zyfYBEz6m|F-Y&vY97OwQ4aJRLUTDpwi6u2XaCGpsy=<4HkqJ&{9j$h3!M5%y@}R5d zBe^#o&BNc8f8WgPO9sh^j~wN2&np`>?(Z0B0G0F{-MlZEEUi;RqTURB7TJ}k{Q2-) zpbN)$^iI9cB4v^ig_r&!%X5!0MZO%@LHzzx{Ipw)Ybh^G`595P=EvJ?H`x!DAcScHb_pnf89pNT}s5$>q zSwwSqub;4?m?C4uQr$f31<>QBi?*WWinq!xrgD+KzZQy$xn3L^5-JdU9f_u7L)0B+ z^%zxCttKu36u-W#4^=r;NYL33|8Mh%G9SW}qfyD1HH#Xap?6m|1%1w7xP^og&|6o< z26_xKWpHvE_t{rzV{0ky%#3WUmHs}Y<~33|%1-x`_$m9?HGy>lqBxmr(V zK2e=ol7&3vTuOOkEb77J_}$x~8O&&PEdj>73Z19{SAB@(WnBrV@;SuDnER)>!=1Un zPBbkqj%-ZHeSwk)K8#IM0cTxXJVPFmtsgj96f-hj^%CX<+n1LW#slGEkeX#KN1{fh z0^huax;TTPtB9>sIp(4ZMkYQ~8510+aS2h99%NHJNDp*)}jDJi1Ef^?>&2hE$A z=VXMO1H<5_vT#jYa#>{y;hT?IA0~-r{}$dqodH0M4t(+p)BlKcQ07us`Ys z-+|EAK_LQtnD^KBk4Zy8gAD~6um1TK1wRYG3pI{boOXd3;Nlqf(m=n?Mr$ZrQEQ~Q zLtw}xEGe!Tm!ruMnnv?;EJquGiux!NXgvvugXEM^iF74?A`2ian8PY<$TS!$i@Mf^Uzr#+Sdt3qE^(&Kf?o8TW`Jo10| ziJ!_?E+QIy;ydM&n~ittwwv6K-}{!$x#KqZa#vxP=Do5%Ki?jnKbP>K8I{jr1$LT- zuUkE8MiydF4Mtron{7j7EGuE}mZG_74Vf_B!p{iUq`hg2!f{8O)JtuX2$!@j3R8XXP=Ny_w;FqLEhF|9Xb*2it6lOlL(I@{&9X0V-tzWMp`&+lp$AZrK5l4y6G=EUR* zY;5k`d=cWM_s{-KGg-W5Lib)LXUi7C+G?BRPj44-y04!DvS z#K+{;?=xjVHup=Y!lU#OR7y`_r(#`oW^2xxuhIlH4JQhp-Msu@{!d90_c1%G@-G0u zKKuUypqbma*qR&uN}2x;wg3My_8pCX@n^=?`sP;uHL3ky?bFzS+LfA#{y)0r6{n>t zCVNBfKUFi%cG5b2m)A(S+-81OniA45F6>kwf!DU2~BB(^CA*P#nUacJs z4{RI_3%7Efv9N-2z+fs6%@Fqmj5v6fyn)~m6=zwT!}SbBwriQt?0rCF3h#(S0!oLw z(nu_F+@UM3^!yk$+Q<8$@`6m`!`Kpw_!d`O{mOrGWRd+wBWPG3xiHUY9jLAe6^l}xh{=QvDaNgL91W%RF;#GQ08Sytzn+&ck9K2th|fD~H3_^Q#%EBI~4)bQ^%$ zn_!#l2I{oHXdA7p#KZ%%yALd@5>N1r}XJ00|;K*Y>h!h^M3Kfe*DMg?;MsM*o| zx$|xUz@hniE@;AZdZ=1Jjy6&~-;S~0Jaw(Q1r3%U3}+TUXOZX!Mm0D0Za}nREIl=? zU1)uam829@@V2lUKJYOc3~pH=@0A zWk<*6@~bmJo5~Rda4&E!+41kJq+lWw*qPbU#zE7G)mT(s`I_Qy};YI3$*m-O6Seh)6WGQ+Hd?Ui6VW^KkEfTyE022DTJ2`K) zAUhQ!(6)9_OIU$;_wJ0S#Fs2w$`jAF+ysR&gmi|ulV{vl<>xB(smD=@NWDF~#8$!RHWbsYPy<%Y$#=L#|B zAC<6aLJ}&-D`;p_{fW;yZ0o^*_cfqR(*`Y@^2*Ry@#Bqa+*oQtPn>1 zB)S`f?wsssc-o3-W0>|x6OvRu5zOOx=XP$RyB6tuMzJ+#22KmV*i%>jmp-wAEIB1PXt*vO&3Bz0kdaG*B2 zkiejhVyJwIaH`b%QU?O7XsUSxJY44qC<{~ld|(UolNV{79<31n-V@fSW7Y5oTccfh3Rrhz)F;k0dW!L2ADyqqRjF~>$xI)Qk2VEkr$7Ae519Gr=VJUVE>S!O4R@NE|y@riNZ)I04!kfai`_hqc4 zFkHb-o9D1=4-_Z!JV8u-Owbfd{z+KvO_$#}swmi{AH*Wj`+B4&!y&ju#$|Ao+SyoT z)W*@+W0pbC2n-H!Uy{>4|KuFhNsg8;|0;MWTlV6wf(GjXtLe>-kFrK-Rz^NAP3>f3 zb0>xC`X3n0imyr~2e2ftE+9yM|L#wU^<;5{Rw$hZrzjy1FknanNClrn81)`a`ltBa zG3a{W%P9GVX5v{xFrlcqP3yXsiW~)#M7&)G*fPZr$AGl3A*4_4A6{`nDObz4OM=Jg_*-bhF3u$#?q0=1ztp}q zTqh8B3I_4Yq$^fI707)|JSsX0c^6<`>}`bxV;f(@siQaQe$G>rlgH4hwpLDEx1koD zt~C{pWXnFWsH3wVdZWNzY#%nZ4=;zJ5gnNIy!RrqQ8qM1|9hm|i0*tBpJxmYLF!z@ z_^`9eg{7R`nj>8UP8sGr z_cKm5xb&lK4`hag$>X+l&=50;oEZ_$9vP*Vxy^_%t;Lt8D% zk{&3*p%xXl96>XIB-|#Z#j~6xGhFiIQq$>1fSrJ({xCDXn9tG(HN`G1eyy|7P7aO;wC03HL_{11& zmK0_(H1m51LBgU!W*n(=gV+hyUF4Y=IlK7DLY}(t$oYmg3L2_X6q31Y{sBtLejp)F zCEtp;2FV3-x(L)W~@%%7y_TN+`9u=h<4P=Z(Jf|=jy!vXmjiypT zBt7g7A2bhO^qhYM?7=*;1~FkvrsXTV6+7F8=g-c4!3NtF(=@F#3&rZ@#_G)ge02y; z_|&wN3kf5o$%VVp4=yH+OWP~%d-a$7T;`2Q59?-{oMOq%x(<=x^vJC$~669tqhqX4lVJ;NYlXk*DvpeuUPZ}gKB9MdGvu(AR@i%emY8L2a)Tc529LR`jO(z5}y323vWUB z{9jFw!rz*e`-~nFH1*P$T9^I7Dx~`Tw%fnQfI+1DJ$)EY3n(2CDs4PB20|+ zz?maSK??qr&zcWOoFVB)jTr7=o%ZxlR98&9r!RYYlSlwo%{kRyiVR-4?+|-y8<5}B z&kF!z&!lxP=04nRtQT8(Ja!nz*x9^9v;N+Ept4B!2Z}dMn`@~D3(Q#$?2+Ui6wR*% zd(ckucDCD#gG3rmciJ;PVe$ogECK_{dC)ubSH9?}kvQ7cd;*dMK}eL!hLn`6hF7?G zt}RrE1fSvUsn$1x>ceQWF}o!%V6`Q5S&p#rrQ(3xeMlx!c0PgRFMMnWp!43*B zLx^-0+nXl^yyn?mJaqBj-Q?<)eLf=>7|`{HK(a=8LJ`wvgy(q zKNy5Pa~Fss<6iR3{rB~PzTydIMfT1SiR|U|y;)Q<` zw_8V%pcFp&J$|QXv2_0Dy3k@NkL$^4NSp~>ke~j5zBdYv%CbCr>O7DBoz?V|M3Ew1 zDu8WZJ19tf)y{hLUV7J6dh=$fVm;R5wx%v^vs&Q`^1HvmTf*x+(v^cJPQ zE7(+rU_g~Y6d4wYpAkeY&{E}v`)OxVotSeuaOu78_OYUcIuf`+%OpiGjgdcnLO+)q zh!oy13E)ja@fdRW0|9aZjI>wEj6-_|NM3!b#Nif!zOXGvZ8f(IS;)c=sh$Fo*5Hmp z(I6N|{XM6Mh9|fjLm3vkiD!=0k~Ox%{YMAKsBp0Xg_{!kpE(kBkFj=S%^PXZ1wvyZ z!p8!wC0F#xFzrIs@PlhXh4j%%fn|R53zNsjn$~Ox&g$S0IBZ);G=B&O0Px88zxFS!^&Nh*g8!8Y{AGR{=-b#BJN(DxV`Xk* z{2LRrHn%bV@14wFW|-}N-JwrSPtWk5dAM0)+hvn2p+_dCKxCV=fgxZ@H{3zSWhJ|f z^t#A8_DU+3=XV525Dv$N)Jx36mfnxd?i%Rn*ST5Vx!yb!eg@dfHCm53NY7*&kj<9^ zQSyHB$HnD6CziJb^Z507Nl)5&4vxlpMA68xe50cuE8$U@ed7oZnD7yTlNqL=+NFoF zcdhfTVGiWl$JZHwCkfl}p@-s<6-Eun&ouMl=+AHZH;%^Qczb=G%6C^>Y&<&|1QR`D z1+p#Hi*qG%811(w)(6*%ck@BIEuB9yN~2|k5qb3n0nyZ*qfAOUGseO2b)%y5C$ z(d3+Y10+KlCy;~(_JM=bhd&l4X$3OI2jlgw;J$k#w;JY3Wk5{_Flr3KKZeMJNs7(k z(SA_i*3X$bK72_i30D(n$YZQxmSV$9k$ zgr;xr11!_xp%ARcc0e&8*(1n5?~C#>8D|eo7eg7}O}1lKri9gxnIJ$SKw=W7KVK8H zH9utZENaoDBt6MZ>L-8)`5Xa)^ZFvH$TsDYhKkdKX>TgZ;_hq5PCG%$l#SupPh2WzI zTW9Oir8$20sPIq0@8I;7KZT~c`IFBc{`Q7axinrX)VC zut=Eskqm!pXaypmtzWG2R;hBCQNWZllS`q9Fc-nm!%~tksY<{X;I~%RE*pDtM%xi|he%R01<{ z0~|#og;tK*_`q|(fXo(CvXRICryvTA?Ye{FT$F$xc%c{VtPoHCNSYxZA`FIiI=2WA zYNLSJQYbbx*w9x7S*I-3YkC`Al7lL}@3&TiiAefrB|x5?AdUuRzZcWp_he(A^8j~K zKK5We!&rT%>N$(iZ&PoC-{k(cg~QxB&#Qb9XKb^B{K|T~>4SrRjeDb(VS<(kKF%Sh zKgApgh&{3dA;yS36{5(#C}t%%Vu?ah-`nOG+@2tRqQ~Li<^k)=bFMUXgg-w?jy^o8 z3kgx)0fhbK-O$Jy_$cBF%1=A&ZLYc7PV4(@eb0vXv)DgyH$dbjxoeMcTJ?$YnNjBN z${GRZJBIC9NLh(E;j{MZ;HI$X^A%(Z1>^7=3jF11Q`#*4`rGO7vg%nrKb#_dZ`rA#Cth;Bpq zXz&=BNm785X;Iwga)ml>iIj{LT&s&}8Cp_OtAr`~?X57KqK^d}Pjg@cKUo)`<+ z1*XykL?V;Qwqr(K}{7pGywU|dlY{F#^@zeOFy|Au&K?KTA(lZ?{Tg~pZo$+-NOjC z*P_(*=0xNO}(^WF*3DouQPYEhTnPEJJl0O}f8XrtwxD0aG(Kk~t5j-t4htLnw zfMX8g`WQ=du%omsY6foxIU;e-sCu3pvlGECNUwL(M_V=xQx#aCPMGw6)075G3~NrH zqbZK7=vueniq5T;LNC|Z0_W5|D}hA><_d*BG-XNz5m)I7fog<-L*&%QMAGMRv?PvDE`Xw&FKvkjoVL0IDJU zt~`RQL8u&RFo$Ga<8bx^reLAu&Qx~-xUvGU1!K*Wn|k#Oqz4+Jt`HsyVDm~{NUAbo zuiUF2FDEyz4o^GCQQO_i?8{dQ?rVk&nx7boM2KqA>c=H!H}*_@d_NQQnMSU7I>jT4 zF|!DPJvfvs(T=j1Ix!Py$a*RH8jI+?VgqTw?ml6KBqn7oKIZ`DYUC0zM=}PCf{o;W z@lU{qB${F@Pv8$KsY$U&at7!`HV944R#0Wf(6~B*L*ue8YPQ2MqvTxhZlw;T^Aa}6 z0Hx}^s68QZZ2>XVG{rotlzlh_2o3}m%#}w-BXSsff2mV$NoMm5z+r(Yd7^xzlbAhb zgOVoeu44MOEJX@ytwBFK8RNeSNrS_}}O64p%Bz4xuh+g`izY8uEPV-4{D^S$+EEMtlE`O$qzxz926>80CCr}iJ z?3cLC>un=*jx?`SrR!eoL5&^Ff`i$x_#zY%<4w(`{Mb2-v_G%8w7EYYlfSSxo}ZIc z-_M)W*`KGQho9`pYGAHtK9S-q{;6Wj*$%LVj?EK6wo30>*x$1EHTJg?K|Qub|03>A zridiG3>BUDRy2wPTksuAbCH}B2Ps|5Lp0PjaJt?0d8hhJ;%u@S zMhnhhBADcPqS@s3xdpav&b83!@yrplQAA3-l%uwxyOKW;S>b|Qwia{r%z&#|HL1MH z`L5L?oj_DOGX?OYA18HY8LiyK!yh_{)T#iF)8@(@cVIZwPv~)OW2^2PH&=eUhDr7a zWmgGQCqA}ip+pHzDS{MwCohk0ON|+gO4u8fl@MJ2}9jgh~;v_!I&Mt(xF9Ny}3-KL* zy=E!J$t;@^^UOilGg#V`4LbH!m6{{$(wQZntczyq-==UX(N|YHKRT^l~5|L(cjqo4-rFgn6y6>`y;WDk(*NJ2|+~C0GHs zP@etaMb3iBvCepBk%e;GiDx`-@_h$3J(*@|4{UKvBFGG9|HMQKRG~br*!jDH6o_M| zoO}aEQP&%seRfeK@*}EE4^g9r66C`9<-wcG^;Nr6zoIusOvBVXKkpd5aaz%{_-VU# zwQ|G38J$@fnv+bQ@v!ah^CS}?-+R?#A!WC-{3QYOmHAgpDzS+h`vT>C^LxXQacIJb z^Y&|0B*W^xJx%*ddb3KSE`jWfYTTTXhAEuWsO7qM-Q~(=ll^O?*CUpqb{BC!dNc-8 zF*PcBjVR_{@DP-lP+4Heg+ivrog1|X^I=`XNTA)es+>P^jitP+<8f^bOI|Z1|5ALh z^~Tx?%XgVwf)=@qf|ekp>TU=UNsWG6i*=kELpt23K&D$fGl>jZNDDehm`;<)W{L`L z`*zJ^16w^SPza|qg{vB)^g_*7gwypPE^%!n;RtOj=&k5!EQ9kHM@Y@zo-B=D<~hyR zhHhjv4+Q=n)Qm}+1arl+uQWXj^B`|kej(aO&lLX0hBnc=#=C#Z(1m(j&V~lE17U19 z*$Lz@WDr+l*$Sg{^jfn!wKB4?c^p?Og4e<3nY;Jvd>M3D)OH^O8-Lr@YR(s}hcS7( zcH%)qrTJQyCsleUXJ5bw!=W{BdWyR_ADuOiUtu)KiF3}(R)BeR)n$Kqz}||xWssz@ zCim{O6hJ%2=Bkx5(haIj9)r%}bJfjG>LI8QI@hZIj=mc>)Q>-P74*2f3=($DZoA&I z%oWEUffleFU!QKT?m8C-S3SXQbgr;(yoXznf9^G`4zQV{$T#lT@RC69|4d>b&Nf0L z5Gc6rgS3d(dT$LDb~>y`!>X=q$AVJMdDK3@NejwbWYip&Jd(m>cwD2xpnOACT`@vv zHkT=qpRvDk_m+CJv{r)M5rs$wv3Jis+6wFWo46gZYGfZVzuKftcw8ou*6onM*OJ*> zo99jyMF>#39>_g`F8e)~pvAvSxWOo`z&enIpHA9dLB^|!l?To=w<8%uuh_Bsz%kox z?}U75{9OzP#S8cRO6H@{y-*mIYh80`j+&>0KHPcoWevPXZawv<#=9w)H4fpjc~n^r z-_ZGS(XUKkiB&R5)=Wuaq&|caN06jYn#|*mpp1Lfu+OV5_LAdnqbSmZURDHHgvE8J z1~Dd|_hFjr_4E00wGQ|G<^S8iR*K3>@;d3vDoq@BJGcbBEfu&V-d;JnW*;Q(?5{nZt|W=b)127u|_Yh4m}a zk$y*2g6+$Kv7z)Qo%M?Fj%2Q-^WmUVy9@Vq@znIz&EU-&bNkDJ70~uOU9t^?)eFly zuW-KR&CQp95mZ*Mdhw`4rH;RV#avQmHL)+R@qiS4eTEwl$$P);BbxwzD-d)OY-iN7+!j+d4b^*TKozz?j<3O5gq03GPH~ z^lLJ3r)Fkn_z!zYQ_>NOHGwCw7jQEN4%dR|;ww+!qL`Q$4Sy3dd?Ol0-ZI~>%0_LO zuhWlUINp7I5a*ZyOm<4kWH9v$9`nQT8MexI08}ov#)c)+>S^sDP&^-jkAN}$&6wm> z-_^8leGd(W#u2MThY9Wa8X7n~0R01ji9k$wk(|6XZVDcbgP(DnOdiADzs5+pz&CkW zJ^4=t@?piE1fhtVeXv;ZQIeUAxV%<=7jW5X3iQ$*TKCTO`deLR?2gw~{93xREmMc%MPnW(H6H%do8`NGUBO#ctpBIO!Pe+=F{BkI zh%ihUWfNOOBXz&rTZ8&sat%NN^c_dhv5?u-&bKWJG3&-AlQaic%?Q9{7epeypz@y( zNC`AVViM>*l#&G{L6wUiDH^A_zCVwRKzxx zfJ7RetC#8#WyAyn5lmd~MSj!<@aO=Tm)xsEXDF$eSOX`$zvV4jMpaQR$NkWbCk*o7 zdjpL`{WcZx?=BSf)b+zN;@+X;<*$g=pwnY_f`QUv-o?erf(z(KuYmjVbfZ(lLZd}| zO;YM^xJ3s4`sRLm6650)NYdCA%@K{&$oc>vYL>rH#>Ifgo+q__`8357!RpnQoD-np z!mIs*E@iMvAyg(zXKZyq5=}eW3lCk*6pU`$Lo;jI1Qea*agiJpu1}d;`?+M}5^Hq+ z)&N9cmQwI{h8ahDJ1Fr!qg%0G_IS#`?Xs8cu-(B@G=!D&jwjB=h)VB_KgPkQw2YHs zt?rRnx98)<8nOOvgaH(Lm608#{1WeOQ>v?k2_wyjuB6F}VVvWiTz-67Fh>MwQmv?5 zCz~7R+*%v*0%j@GcPXCueLx9s7v{#9;@MmXxZ_P!EXb0X!%2-1nE7Aaa%J2QphB5R zRMWh*XUZFZFe%M;MrTxq3*|LcH{D>6xW$GF{+cK796& zOZ_n?hrnG-#(UJkZ;6Ud450bj#o#}(1n{B{bh>c#C5QvVVQA;b{5y>NJP<3xZZJ0& zw_gh-T%Rv~kHB&?kpemyScTHw7Q37rsHTXg_l4%gOcMa}fMV=Isd7kaDTV~EH~uj2 z@X+$?*~xT&@?SxsW`^6@kekZT@Dz~3WKA2EgwmEMQjeB2zToH!#JRt)wer&s1t^nJ z9W2r>)YwsF+aa8v%mxurH3&eKb|{3%l8G6?<`)vAAyCC8(Mv7D*nkQ`#8So%fgtH? z%!-ld63@`jj~`uXX^qrP1E==|jTCtup*zi{kKvAjGH`Emq)b7|-m!|XHZ}9(-gr|1 z)w}`1gOjg;vMK}{Y|^}vIwMi3nj>3ac$ z?~&cB~-5l%@_{ z78u{iHS?PXntedmCJX|(4Xg-&PW!|@^t|V%JNV1A5#2qQ5Hi0OgC2qpa`lgAT*&Cocm4NIzj`EO3POji98K9$B?pWV4l5WX?sLh&x$AEk~0cCf}XR;tQ2ur+5+ zS3L}09V2mWm}goVKcs}sFg-B+g3V@NfaxU_ie5M69_-ObfQiAZyv-JlqQoR7y=yXcnxT z&A)T51()weDse>h-#R$P5J&w_@5F6h5l_$9scaq&f#--*adX2s=ZOKLXOcmc6OxjA zZ;IdwQ7Nnj-M@38j{b17^WR*kw&-rR0NH3bB3kRYtm1z&-oT$&|I%I5-e3we%ZwUKnx_}25*%nmol3z4RS)}AKFzQ9(+oqOPVSdV2^ zLtBnMcwewbR`$3%(Xc=>s;3>Y7t+Wm8U~{Sv)OY;4C2i&@Io`@Qm3XVGZTXPR<{m@ zuQa0G3W8W5-wxezPt{i-Mvt11J9{|^nRqSP@DO~S4sb6Jn-9I3E2Cf^C098yPwbUJ z?Xf6jH8jg>`s?Zb(Pu{NX1Eyo%hNl)kFd*n!Bai8W5{suiv5WATRfWXZsB3dQ;)%R zsGn0pqc`>i4d$M;Pb9&JwK(J)KsCOFr@3H;%^$1N?bf1G>P}2mr;T2+;EA-n3F2XM z{P5C2q{LePxg2R~u_o*kxsE0%gf|nnP_zV^rg3xNoI1o2I-fN4d{vA~K!euB%NP4o z-$VT*Z3_*+eXadi62fZ%^-x;XoGAJb?%Zs<%=9s=z>-zfQ=WyrN|hfv_Nx>tt78?m zR`=`Z=)_dbn~Vg?#?rvxygu&V-S9Mpe6(bmw{q9`KHmHkI@){JtpHU;3GO9MIivO0 zNJ0;ZqsxfT)M0YQ%!b`C#8R(IR#z^T;-ZFco#Lc0zzV-C-Os}_RI?){{l1-vbm@u~ zYNb_N`b9HEBuKT2alOjL(^xDofjly^@vO)6d$rOrXOG9-)Lc3l8*8sk2Lwe(I2bH6 zEuoHSw}tP`{@o6j+B7K!0Tilb#OihCb1v-jUmZ?c9A^1GhLxRE5|z$axNJCj&_sCk zaqdTGAk~J3^aS*l$L*tcvO=3QZKmaEE2m;vC@qANaLj`xaWAYmqJE->Kc@Kj5Or)O z;%!ESBVN~iJFOU}BG2oLT_ky$RYVwm^CNDSy|vk_Q2&Rka}3TTYP5ASv2AB!+qP|E zVmoha+qP}nwllGd(Ozjk$Xb@yI-t!L5368MUo1U?8xM(Sa^v^<8h z%~vJe0YH-(YM_;f-TA2nu`FCXotyBI>WU{m3s=8gefZu&A0Fg)c)YegU0L`TaC&_f zZ8sb25_Jx_y1|}h4gPf>8u=nz z;aP{++5Y)Ex;3|wM-F>G1;v9=>VtXE=MPK11bk^Ci4PD&JRWy6t5%zCUB)}z)Enbm zF3^0%A#;R#t4n*A>9J1bW5YW!bXz@_ZpmLHr-3#iCraa@-1R%;lp}<75W61=Prs-e z(@f}de8+pNfFM}+WW{8NxmGrVOg)Fyb1s}QK+eK(3DNiYI`jQ`2QuBbxa`5yNw!W4 zRI%WgNycRWvx>Ih+;CWzpWoh611xwIq6X#tKo* zfEeYNA&@5|F2Vb0Ev{D>%sF6RT)7bt#zr5tMT)63p%Z)22 zBmHP7<^}jzz5~HQrkO3R3~S75+x$I#JO5RkZI!~5DxK0oJ9q&e5O5!$Y+h0pUygnM z*BRdO>HJcQRW?P8&GliUGnw` zXH#o)?|7p}xz(4oqk6s5!6t(#TT4w(IZ}x=i)1WstRS$=#s50STy2c8l#OnkE>Tqh zs!?eH8`8DX1*x)4=$!#OHlzfpC|{&uAxx-T_Pvt98sEA?l0IgC%naDoX{TTCQk(+@ zH-r}>?cgOT&H$@iK9!fDXKANu(@0i* zg$>wl(F}xTYN-Rs=sf$uC4U=ptt{4a>j_h zAJ`DKG(T~dkX#Y5qa3CEH3NKA*E{(U*e=q{H?yU!OV%Igho^{sKobEB0q@|D(12=& z^uWvhyhW&CJNEBr-v+J1r*k##tHh+j58NN;1apCK)IYJ411c=Ak;QSa&*6ba1H9b3 z{_h>8W3`6P%|SHjx(t>7Rp{4ht}MTE{ba~fF7JEC4c+NS#md!jut&^U%`6n8xXXie z_mta}6UAVvd5$;>xpf8Qv;)L3tFZdDZN&Cgr2PDnXbOJ$zhJu)x5Abv77no)+uApq z$Q?T^VRLSM9LBjyAe(l;=IxsKyHBHS=Vzzmh1fg2Dou;AVa-&uyT&6vfLrXibR#T& zS@!ix4frs8%#h6ZEnA2>3t zspEgBCiXu#q@R{^yPx>BqqCurwFxaV3p49~1+$m6wrmgD(0W>O1LmFzU|^YxL_WbN zBGS*Ux2;Vh)2D1P5(Vu=@HhUBC5aa#N<0oi{}Jmt=R13|0ckQ!?96_#}!l^(ooVMVPd}z zIBiIa$9N!~h1rL`A4r04z)h8d4o-HH*^U)2oIRK~enQ z^$GK*XIACLtakWd5$*IUs7JZqto7v{o`|n3=$ZpKJpP>7U2Q#f)whNhPajx-qv22Y z(Gl{~PX7rovErhqI*>86^5Zd@g_KOh8;%5|42ApQxkR93t0 zdTqVdk!^M?u20k(B|VHRIYG$fPu%%bLVGg5^7DG#Z%)zAxe_SnQ77k#=nMkpa#)GOB!pNZi>`7J;Sh3Hp?SONqO zWKvWiaFH@-GLGj?oDLkn`KrJC@WZ)BQn_K z=#h86xe%d0l?sbo$P#*t1ML?GG&b51QW25-r_87qWUj+AxY=LuhNGKMhi9|8bEudh zg)I~yfe1upGGS3LApu~H!g!UacOsz)!^&&GMO0TCS#9L<_NL}DO9*dg^nGcK&qw!B zEX{qvzZl826+U&bb_?CU*QjP$WtPP9N}CVWM4BpL?N)%kw`h;J!XbB)2HWQ0)QU-c zCVC`vRfR-8)W*V?MXyMW0=k;2__iDqgtWw-ao+D>l{{1x5yVw-dnwWfl%r9^VTTsk zVj9C3xG`#veU09 z6IG3|-tyV!^PeuX8lBz7oMCxUKz-pV8vH#w#m|Q-fQnZ~E~J%(M07 z^;~u(U7`%+6HYDiZ@2b{FrNDI-NOm>lz2lLFaOlwMM}Xk`tPJH0o7TPh|`oa3p6ch zK70ns)T64dg$Fl&{U1STbvZLs{`}RD?|rd~&5ShCi?Q)Dat-?CJW1pDwg2B|6|+Tb zg)X^**;;ZUcjeHGvp3F$_xM?qCq@_VbIXj^K}p^7uey*$de;;?{W|M`_V(~*PHtLVRF|YF*Z(}WyZ&7pR~!v>*uCaFH@SBfHL+qptC>hBQXf}euOTWD zb6iHM{ZtIEV2yd+&q_6M(9+HIq-ExG%+a~BM`T>n8k1|-AduH`J*ahI-Y89f6bkAp zTa73!0Vmi0q9O)oF~nSebw{kD*iES4LT2>!6O#es)**%0no*~!53>y$;=w`>|Nfly z{fJ!3_X*fpVXrRPo63=s6+K5B?=2f-ZGLSgDJdCyTsyQq@#|A{H@GW>)lniTBI0o zVgLRc@C_~~Cs}V_RCh>kg_(=JRe4g|Wq-7K7}LNe4WK|0!j6P2Q-HQOZww-n45JAT z0{YjQ@~xf7O`|RNIkDu}`MW!Hr!%1c)SobH&ATVD49#n?xy7@{Y}FL?#u4wclIHNJ zY%7*}ub!MqFxYN>WsJ^&I+!jz=x>>A=jyn6hYdgsy>UvzUUtmGU=_z5h*rJ2doqT8~s}?=PZz~s=!4uig+kumN(zN(&YG6HTJL!M{-NGyOB>Cn2 zhHa)>Sr5$*7!8W{Y&0jkGiyKb|4kOOEUfIz|4p#r(RJHoOPPJD62mWiPqocDwcq~Rv zm>rtubS6BBpO(Ef9W56`rqc9@I9eDNKyE(am)byB5Ct59!cq*?2K$1!%drpXB(QS_ zYKT9C)CP&jkp}*SCV&LI0|7M*)dAZWZY-pELge1uUVmrkWGqZugAiAmE=QpC)raAW zg$M`%kzIW(7pI+dqhah93*V%~S562)%_8kL7fI~O)R#4JI}}l<0p}euPd%D**|nJZ z<=@yRu?eQYe)-S^L253FdbW!@Nn zbE*Q>LZJ^sj70$sAYP#Kq=KA<=0yR?2bp$s!NAUl&SRbP3ZwA`;vK|$$r0x{jpLItTDMZ%kKY0;n^Nr=io zA4_fNMf9>(LuQEED_aUy=#9#xg(SSm=&4nHF5y0fkyK3NOKEl<#hKd>+KnI1ILWy_d{;DD0wpT4-{(eqq!MJynhS#O+#_Uw4ir=?618yb?lJeLi2bj=Y|_{LPUcB*ue?UpRMK zp~%B6@w)c&ST9G*ZJ2SQf)|Ucl&o<;i2jkP(RTeEhh1c;U;hY z88QAR%XB;C4_6=jHphe*oFnaRe$lhZPAO<^z!C@^gPbEyo*+5k^uhOp?g>Zq7vo=Q z5*1tpC)}jgHOoRrN}k1>=r5@9-xy*DhBF=%~v?0mef`ZS%^93i+{&e z^DBukwX1NKphA2l8zmZ~XFwHu(fq@}z_|_+V}Y_SUo*N<-UXB@d^hSO1`UypiWCOm zNVDY3o=XY?Ok3fLM?)QkEQSnC9y7Fs7cJ6^w-Q0iw~XDclOQk`DfKNH0P~iWspU>a ztT#$H+!v~@lW0s(zK6(1yURT4nNE&2p&m7{`L?fYP0sqEHKv;it7XUBF_El`HXNn7 z3Vr?rSs@<3^*-;Q?&1OE;@Os_x^6!^abcu(jV?3!8nua5)?;4%Qs0O5hl| z*v>zm*o^Zs)#pEzxS}$H;u(^%4ygTeLQ6q8ro`rmtHU|6GB$HXU-mM0n+udDinG{0 z>kgn8u(I0Msn6(~#~l6x&rj#T&H@v=fTyX2z5bvPN(|fu7THR1s|TL>?#^~Mc7h9A z@U{B56G7`!RIO_u;Xeup=Q(zO3^^XS!jOJ%c~1P3d}3fthW3>Hvtzgzd1+cX4Gffd z$E|J1M@8(3xHv^0nyOk1doIIgHg~%~UmP}bE;0w}Y5$fZBJIVg)~O^{voQv7M-wgwt6!B$8y z%aC<)MkInVsp8|atD3f#%F~*oHm#ldtY4elJ!%w;8fiSK@#@i<&#N%K)fd4&+Cwqn z$7y2=d4{ko=9)`>ayyJf1tn%kn@-kCU5wv)+mC3B+$NfML-Oh0I*vXwbkLi>o&~>e z{(-IZ#?%5(vDX~)f}KHmjZWw8!d3|kCY$5GC~N0_2CE8YAjkq;ZX`Fn zwX^HdePq7SrKNArGAj+qa-P{?B`Bq-wR6$NAVqsQoI}JpK$k0x8PU`srx@G4c^VbJs#JUxF65)`oHN` zcq0wMii1&hLdS+dErb5q7zf-!Qqp&H27tkOE&4Hzw1jw%w0V0V=g#smKj>t;uFVpZI3 zgD6TrCVNHnYVKaX_HK1W<=v`4l763dC!OfAsax9m@ z`9)xTvWtL!2+~OuE#?L51JRT%L(z@zP9(qVSERY2UnU|VfX?a1aTGB?5n~``BG-YL zo1ZM!Pfy9KG}e9Y+KJQ+TAFl1ahY{;XZM72`AMg(CyO|e$}+Ceow~PibQf;UBN8R` ziQWfwWP$8YFLi5+xW_=6WMen)CZSRv(Kx>nlgEjZ_-#QjNO`a$d5Qxh?jTgUw+wx2 zzwHO@MTD%449K3RT3&|ORGpcTLO3K_q5W3?=VNDwaB9%I@+`#-fp9Trw@6XMvPaOq<2=vG$C}=`b;n(VDPVFSS~n(Q zdjzQTA0PKYAY?dAGx2L9hoXv-bJDC?!Fv{Gy9FZ?%Cvg=YK?{K)xor=3^@Y;>(gNI zksLIuW!0}Xl;q8kQ}HOV%#&b>Tu65i96nj=9uwxo>>$J9NBnlg1N!9mwQDd!RA&l- zmB`+YVRXszm=NT}sW}J)(-LipvIJpI zW)S}DvVaMyM-%D+uE~Iq?OqxbHA7v4j4);wcU4fWbyW7mrKhh!Tt1ZL+M5PnH%lg~ zcy&I@_@;_53Qg7!UiY~)^ZYEwE2 z3Sz^YAryz0+>m?*$`l)q0X49HZc=`t&g7ZWs;G1<`uxUQRoGWoX!R!1n*FEZ;Uf9J zc5G4`E_?XC^KLRW9_7epj`KKE)U%n4(^_!CfYHZfB4k=Ki7VlB%mb?jtYGPF9MBk7LjN|H197b#Pc013K2 zLbwM6C}d%1hOKe%S0tU>8ve7C9+nIsX+DYg#*iZMIXtz=cKhVu6msw4g+s#7ws2sf zeB4hy=Z4UqvJ4?^|JBUCm{bhk+WOS$3VQIMuW#B59m;|G)N`JkfOt7Jr z^Y>8F(;`98d$SelQu_%=E6Ly#y9Op+UQu7Xhc)f9M=6P{yldQeV))gdJGK&hf47X zdv7)qj&S2JSgCPlZskBuF+2@ZUI@awN^72I7<_#7?^7#G zWv5*&HVd^49>H;K2j=7*MA~-;r94+fnU9|Uw$_1 zp{>>MZ{elzT5VNt@zFj;BmS+uy%v}D-t~!>StTI;*gTRRFT7$DyDgLur72Tx;@JXw zIJ&0&=T~{sU?T6D14%B@GKJ^&TA=G$vfkIma2tcc^WQK=lZ^Q$nRNeP0|oAmx|T;h zd<~{ZMge|1dj6$%4bS&!pjCa#GbH;WMs(cqV;#2C>SL3BV_Z``#YeQpR8$qg>6{gm z^lvDqUazDtz2Bd?y#WWMqh_NJzBvlc z5zot*5{2bl{{AGMArLQ;JixV%HgUf=cY0K>{uq<+Y{Upi^X(4_^msgtDcn&Nx7|_a zG9>&7b5m`2*UIX4z&s>qYV9>&(9*f)A{zkrciq7hfB|qh4?k~dI8i#_mfpO6zETy` z=KrqwDA?3;w@{)G^#v<_sr7}3MVNu_+iOfrEFA1m;|VYH8u1NFR#y=a5>?+xan&WI-lIWGYdHCUj2DW1z~eg zc}anurkq+A9yQ^^fQ1?lCr(oH---zr^<)(hEpz@RB*le|#b(>!IEhmY14hSYG9sR11BQ^+TM?6?&CvT>@u|`i`F;16L z`5UwlOi0`>DxicKqAp$3KsjhDC?(?Xc+t9U7T|R3OeUc>Yx>&C1Ec=uwSXrl^LxNt z)zYz|fHe%#n>?pzvkDjHP+ayWk*4LCNuA$(I#~ROXa$J-Lpo*q4{+!~(bBEig&gy+ zqp6Knzx6|)`tDosz%^fMeAP#&Rx7U!S_!~7UlFeM-cPpA ze2;tfNNcXVwX9$}2`_v^%sc?zWYjh)jqHilSw!71(+l^N<~P(v83yF+GPf^rxkiTR z>%wSPuCK7fk-l%8{HU6wC3s?-7zke#ZDBoy9B5u%DREU&*Frakb&G^b+~1GKkH`O8 zJoDczgNsM~5zl1*Kk>}T#L)4_?)xK@IobVaX@)=iu%CdmHQ=X#)#<;FoGkt)PW^+3 zwzGG(u(9xz};7<6`K3WNRXBjl<#Vso9}OP%0)SrbpROU;lQ^3ziQV(`lTr zgY`H#ee78%UvY2OBP%k{1?c=%9mR4~ip!x1yHtWwyEHUuW#1F$Lui__;~SlnXsdEA zd>tz*7n*jdz)bJcK3;ZDPfH=h#XsYtnEMk@%fjxHn2Je`>XVX++X+ZyA|!?(cTcek z2}~7`HO*fYQIm z3vJQrs)+rr2_uB#y-C~s-f^P`OrIF!i+%XMdBJ_mDrCAQe(#=+bz>%w6%an~6jLAs z70@6ClJCADglhnJSul>iJApXZd602LVP&q(;a4 z#Klp%$h(M&U~;37F=CKT0{u81z@kNwl~Y6+(R|L8u^=Id_3Ksoz#TBuCS$4ZbxjNr z3~l!ewxTZtE7s&HQnc2wwxw|0N3RO8PS2*~@DtM>WB;gXQQWN`s`nDp_a(?JFUj( z_Z?~qC{o+bML72fIGy4R`8hH5G!os$$U~hPY_&;&6qW6BCJ=`uURCSIYF^#gG~XV} zh3(mbg-RrZ)4z})vEQdkr(#Kt;xUwl??vi4;+c+dU_D?S&Y{N^f9}ON?WJPE25oP% z$5;+5X~gxP*s5a~J?j@U-Hncejg-l&hG<;kt7bj?n8m~V4~YL`|O z%ho9t7k>la=SBI$)mZ!<3aWk>VG6+g^tu~;PQ3c;*TJ4TdD!&$j;v~qbC|dJd4Sg! z51G35>!6&N2|0Q@gdX%#Drad_aa05ibh(N@0b>dUJv2MVzapyIyp|!g!&r>k-p%QJ z`3IvQ=J?iBW;bHXNa-VDgZ85Ge~wKUl+zlha|{zES>V8t4_?Z6bJ7W`(ZA0VB8_q% z`X6gE>!sxb0wr)Z{3p)~Lf_N!%rWc8DTqSnbKjUuWk8qIe-bEg&s^H7vhW)KC zf(u$&?-$e{!I(kXUI0r>Nu0wpi;;L$wf9irFfz9@la{Es;+`Wh$N*WMDN%rLhhu?cC2`c$FJgLg74#!p#GS(DUP6Cxg62sA)4&*g4QwM9VF7D zRG$070?8ZOKw-#|7#_>0g#IQ->^S0v^sBf70!u%N)jfSGFsvkJg2I?{PUe_s_>vNd zDV1?(vu>owla`903A?|``uNv5(o}bM7}S{R1m+RvP!ePMJZez^lSEqW0t1>4TYv^! zWJL>sWn5nEE(5SF?9w+US?5*n4MCMJmOQQ8T3UY+^hcJUkP|xP#Gfhi<6!~eJu@Fv zK>HM1B1{w_BNHh-T}~X#q)p?f|KAwo%6m;+b(c)MWVKYdu3!u0+cy_uj6#QC7_!1n z&_pZYqbe~o7P%5SyCWEztmTe-`OFRu`srUJ_zo4)V7d4gPl5!RGyOAEqowS%^mJ~A zjNnp_rQnt_FVH=q-EqU$`-8jbn!M@F}{Rx?5*LOc`q&U zr*`7Cl5bxj{=MZ%bQyG1?b=YGbjBD>)$yJ+R>lP0%V0MLYF=RjaEjL)0Rc_@m7{+a z^iD1sM`i*VgYiSc9J}2KaWXGzUpLPBJy5#Y(5%3%FUeM4WNgcI;ac>%>2jTPVT1Qv zxfX_HbvA4J>3}CDwL0(h4*}a{H4Q8gy!=J^H}6T3v5NgF0uMC1RMXz&3(mPUUq%%f zmogFWUA3#rKOEZfd}%}{Tl>v3)zbo4r#B}^Z8MA+4+4jCm1~3OQRSYRr3%#mH&UV- zvYAc(BlK-q{6x3JPGfFl`ncKL9oZP|HiZgL!K>_NL^5ar7<(wAPn9#yB#MGr7sGrX zMfK@{-3Ed?ULyYH@7)E;o=zQM&i88zh+^+-ZlJ+#_NUg!mCSmtxMS>q1J!1vD|BbBa#fYtWaRbN`6?(%CzuQMp$ zs}cA2x7t;Q4zR^FSVV|FIpt(~9##+~oO@N!7_vlAui>(uAzUjAuMW~1e2R_s7W+UK z?Y1V>_xgFy=T>LkLzfNnOCI+)>oUeNoy0eyeV*J$+2Kah3J;o*rY|6av9H z4l#>?eZFTaGb|&O*h$CI1xCUoR#~e-#(>Z2{Az7G5~np~ezc|15e1J<=*qjR<*ezI z(Lb$Eh9n^}$|DO*#S$Y)0OshDJPO*e|KVi{F)TKdmG7|@bEiwb>c?Y@4IK}R>z36? z59x3+`&n=c5U-ZnkFaQ(_3!A9b z{W=3{;QqWy2fWUsdg4tyOH_ZpgEOzr+C4ZW5ny6=RUP`!*%xQQJ``B-MrW;uupPfY zT%?NYw%uFfiNLxRRMqAp5d5URJz(qwhT>wj+E?%F*VQ8IsuKS8fhie1qR~wQ40k>H zPab1$2&{R|zdpStd9K1}SHue7PvF3He6cOvQ zJbVZ#U>1Y!vm~+W`<_M9MtrcXG3V{*=y&2WC1<}bPa7&mu(@kwJ2na6we(7R`$61| zskwrY2Xz!!YAavYIjd{(YMKLfvJ&)LWAS-EzOE)iM-T0MxycZ;$E_;OHxH1sRfm-s z8df6t7`%u1e>sq>(vq&Brv;EwIEWZ(@2LDyWfi$kIFz+9Q)nHe{tj|QdXl8e>E6aP zSoa9m=CU)yEoOp8o&f2h>>@z|nqvrx3azIP>gB|pAMrLjDMACg7iO0{5GtpK`QDI| zbsfWq$CM7H5+o^yh5#7&?I$uW>*4;Xs5*&UyPG-bJ-q^G^@xJz zUlSajrf7k1()2cdKRztGt(xsLY^z9l1gX8^GL?hu0mFD{k%qtAxFhlBi9vf5<}acF z%_D|%q6&|MsW@MQ$9#f_sRz0od6dr!}nPDB6+vz&v z?;v%A`s3pBj1)HnXvblV)5x@vzXwTo0tz|q0dE>e@q+kF^~MeKYr;!4<9s{4gXXza z(5qPC-0c_QpO~EwWjod5g&>?H3w_Vt;4F~oMN{&^6O9!4RQDPgp$@Q)PUz-+`M3DE z1^ruCL)Q~AFm=-bUBB*eAMXY9KzHvXj-X_d2EH)m%f<-vC2>s!sRjri*v($Sy9}Op zgEK+kTtjwvsR!UR?*=yqkcT!;gtTw&TOKJPLe_jP!DL=pB--WEC4@4>i1XY3{T|!z zdsvix=G&EbCeSq0C#2yG9pHwaQDVYHC>|1sM|n}9;V2KDtVY^?RHP~_Ad5Yi6pTG32)`eY{|MX)?UIzmPpazE>29q6 z92?$(31+WkKh@@9VF>+r=b|Z)I#EptLC{9HigC&%=x=$D`>QG#o7?k-Y!s9)UOWcQ z1_V@IVLP4~6%?Y7Zj=M?dR9-Jd& zVYdAD4lvk=B9#ZXXsR_crqszX-F=ag0Fo?SApxM0GQVb&OHXCYI2M#yq)?i8Cgzu7 z7q6}lS=fFsb#80M8;mch598e09B#NBFCV}8(qfHSjqI4%EFhyF1HvHp_2?;M1mPlT z`vCWKmSjQK*)T#H(P|xr6pZpgbrH%D@@EIC*t!vjq%)f%z3wAkgc3{BcLITa9G_`& zn2gotZTRaYa#xS0$IjxPi*S)?t+CBf+ zGHSKEh-oxLlhZcajk6QpaM+rxcstdjq1qHn6Tw;Z>+kG**X4}42~kh}@fvpG@r@Nq z$2BTR>sd3MN>*2Ue(CuB68`$E{U$!4#jO&xoN^1#nH97!1Fz<;qQVPdO{*}!T}2t? z;mw|Zcp9PWz@Qz1xsu^f_qB_1p|L0yY<3i{s|e6JDi+O?HH;2?smi)j;v(CJ5EX;b zHLvvKk+xwKmt+v>&+0r(k+Iq}-*pCnT^Y`B?%J5Q?pDytps8{eqn~r8@4p0~+>lq_ zY#c7M)@iA+vNhKe^617h8~0&C;3)zkB6wFx5yKY(rWmJx*`VjFwPWfS#XvRX`FmaZ zO;jW)Y_KVkf$0buYRL-hgbK)dR;66QXRISXk%yyi(v5Gm zFE%_=xCX;$or*Wkz(ClfX$-+)W3I^qHIB&!nKw_=DsrD+?$h!O2ZROPvV12yZ| zm<7rYBizUj&7JOGq*a(TS>%9nFboc(d@7~Kn~C!>T1p2aQ(i~6pzqP!HgBVF2*K@I zj`|`?L`D`*mYXIg`1cHJ433IUYqRB#rQ#>hE+CHm%)xL$f}VQR{%m7L9zUh%X6>Un zht!MwKEBk^-*O}WoU`fx$gH6G8x~ej!~Shxn3=OWDB4s@!KiM=5pa<2btAh)@Fm0_ z$S=CeZdrjBwP-UetUY&)cf}dFuBtHhKi?<~+&cJNwj#8`7K1gC9>Z4ArJau5en-*% zoL4ytrrzLvs|a+AX+3Oe$X4x7n~3Y8im(KLOjHLP(%N%Z&_;G-&|C8hFU8pd`su>NrKv-Mdh@7jwwG5L+I3#pt>;2#g=nVMMB)Um^Yu^FeRyr7r>idGf%Exd=>D~;q25IyI zC=Hg*^|s}Dw?SPIl5Kx|y)h`)cS?EsM%ELh>e~+=45mJJZX&?|5KXmvJigP{mp=+V zHo`a7QX?Vu*xu@&d9Ow}+C#^`>83xJG6AUw1~>E+2WqbvsHs1}YMW*%2%@$d?X!0W zhf$eP!~E&&w}s~@mlmh}Z^Fygwv&^&N1N?0hVr-9`pv%01a?YmCy-87vDVdQ>^Cj8DY_6|$D-=GUMULo?d{I19~a9cdYvSQr@o+cM~_ zo%ok6;kxmI8qs7_V*ypF%da&i`|jMd-OagC>;4C|NE9eg0F53=gYxI6d_y#=6s5k_ z(`WG2_(7TtxeK*2LwBoqWyZsG)klFH>Q}f3F}*(7T(6I-Rl(6=n9K~S3-2*U;+a`1 z8K*ul-$%#^69}=TG8#2Sc=_r@LDkqSYZ5jr=i+h#BwdO?GM%ib-j=VBW#I!D08A;9 z5-{;FENm^{PB$LOdiWfhNdygL>L}9leYsMt&)?}%Sr}M}xDSN-*hQE}V-PW-ce&6P zt;D`xa{(oNPz{&tt0yEkl%W0xY~&U!4jJ@RInHMA+{*^%%L)JF`JSWH)js3Sizr3s zg!$ver5po>VpD!4cs`lVu_$h2CP~ESMfl+ycELoki@W8}L-Mp8*(gJblJY@U%vfA< zf|%Wz7$I%8t43Py9!*7Izv(K;PBwxS-iW}fkDP!F^vB|F0cme`+4!^^r%V-Qd>j%r z;&ZNuE*&}|M^R4DIUoy27>Q{eq=YKasD=Tta{!yIoy70)>%umKl^!569wF1{e}{hm z8m#S2pttn}yefGE%u1*=rhS-18}Ub9J8H&45vJR;s~Qk&WvAN94IZn`#^4?KPu5@Q z1uBZ!4wO7?j}(ZLsefLp5vLJ{Knrmdgp_ejG~0r5e;iU!vem&DU8H<#`q%c%9%E%*}WCa4C0wPl$>fsT0-1-HevIXE-Tgys$m;`B%*S9Jiam& zM%BYfNd_yTbXTst#N?y1xjfzL(XE}>ih8|@yzR< zHdEdw?Sz2L6~f!xZRKZ*b8+rc2}VMZE!=vM^0%vQsQa+k-_cQ|q9H_DHJRUOR3({Q zgCK{6V<5r}SL?3pSaXyp+VgpctuM8=z;B9NpCxzKaf*DhLMyY-^Vwe2;=xEQDblmy zu%hFHGe=nWM(=sJ=?vIx&1-v1@Y_fi-HL)5&b8jx2Gmg;)aPo*c@2SH+GpE!u}he? zY0KvwMmc|KmV9dyA<&K#Q_-?ADRV@=xm+TD?3bee9O1ZtRg10$+;e`m1Qh>j2k835 zrNJ+X!UwO0QQ|jaqvNo?$gZHpzR)11qmiV>r1>;3A$OlCCB#mnmKo<@0X{`S0S36tfvg1YH!Y!5(Z zcG1akF~yD*tphdYRC=~?j!ee2%k94l6oZPT2lkI$+9(s zJG>Vpn$;fWc7KDjU!QWm4Om@9zL$!WDWws#)yoo+j8R;+cBpK%d-^`**3R~Z@4ieglJ}-1E#ndKdQZ24@{{R=70gtQ2gkRf z!Q!Z|8s-g<`~!nSQ%?%cnkR82l}*O+_=^%j>L@;pi?cv;!QSy^+3PcJDPvs6k%K() zWDymwOA(=Su8Zd*ShjXcHU_V^=^_4X}b&RNOFbR!sal_mt(PRXWTSY zVB*{>OIDy!Y)*n*JpSM#ZLRF3xVE9eZOXG(lQ;3GJTdSyRfLYEqL)IsO@q!hAPhTo zZ8Co3wOPwW7X|0pX}aMvH}gZm?Xku^j$zJLZ!ZROO*4CL+3HVF|EOGlow0;(FIzcGsKMbq1Crb;26IT_zcfH3jEpPLrt4@>; zJit&03u(c>ld50dTRSY5Lpa0IxBWYp2||~ZQUT$)kl39v3Mp?y(S<5X=(i-0N%R46 zSJx{x^e5Q)Lk?el>ZfWk>l6^|wzxMi61BlNF;u^IYY5yE7Q?^P0W%yRzDk)Z^+VfQ8CsWhV10TmggE4!wdV?}rgP5J#6pLDu( z(R9p=rj#-QR#tpOCf635Jx=Up$HfGrfftfji1o~3>Q};cf@c9b8)Ay1dUW0Edyf=_ zg3;3jrn=2G#h4HHJU5*pI`0&TZGS)VNy)GA9isLjtQ!!7JPypW!2S`qR!e#IqqyWU z$mn3a*f(T1*L1L7;YC!F4vVNJtSYya-=jZ=T$z46E6k)zF5SFdn)x04#3{RZQItgZEM=L zZQHhO+qP}nzJ1Plx|O6V52;j~zt-MseOllcefaqTKN8)46it-Df?+na>E4?jwZYwG zz00G2?$@8PzwS>@dOAI}z<9W*Y^Q8r5FO_qFn}IufQ|{laV64!HJxarQT%@lCUez` zx>~L`NY$Mu?stiSb55w;z=3r#e)d8Y`J>(ggtPH}O1$fOi>uF3OF=M*FVEUKnMU|% zx2#Q&4djgZ;q`U?2|KmeL(qt_0433K2u6OPzecCnad!KzSqN^q4)T`+Wk`i{Pkm5| z*9$R(N4)i$$5Q0B?Zm-rr-R7~cFk44>zXH!>++J5m!*ZJug|dtCMPXV7rzd-OeEz$ zjS0jry0}Q#LaN8+Ivxed;i7r>`@ZA!jueWF`E_)_%Gm{(y>VsmW6GV*d;|^JD-Okj zMk`JFKXQ{YD8$s@NRWP^lb8$#zp$2toCaly$ zF#)5Rp)U0+0wUtZMeDp=jt}1E#70*Wc$32L6q5`6Nwe=uPu>}wu}1)pZzi#~9WtI; za7RURg{n`2Rnxy@BjJYPnRb9;1St_Kuf)yL3u&F=>60nJ-40>zhONs<_&lhf7=l4p z6OT=CcRyIA-|}h4+5?lSRl6=arrDRBLo1W@LPxHJ4Z8+2eKr7~fF1Q2gw^bLz?Ei& zS2*rQ>q{k>tRlpT^Y7KdW>1~!_c?R{>|lQXVrs?Veqo}gO!17(shgvzbRHQ zg4vzdk3+e4MWF75C4Hk7M$vSBE)F{V?X}H;4kHW$$5TcIgBjCw&cgMo}NDmsflK^rM$-1@qK643K|IB&a@4~9zIVjw@@Sn5L z`i`XkzVRDP7|@^YWs>46ot4vd(?i2WQ>QZ=f}dRgxiNO}c6Zhi*=F*n#d0WaFrG%m zhc&`=>G4R}e)7T;dJ^4b3kgp0Q=up9JtL`VEp-pJ(ET4Qyi1v%Ni{5qKNP~QCkM~O|9SI3&19c^VD+;Q_RH()h z(Yo;sMb-EXOM(%A5Ltoy!RikV6%^5M$VLct+q%nYBwVMd$L&4t2XjrEJ z>8T#YjWu06RBzum`12;%;>**my;OV-|Ju!1%~Lmv2Gp|D7yk&4P`6%p_6N+goT1*q z5k-N94bzUlb?W zyS{>ayugdjF^wxq-%k2RK)XMwn!pPG z(mpJ+d#It*p?a+YS?Ni_Bg&7}bMDFQC)E#qZZd@tDNzlUM6ll?8TZ)LaN*@~fj1q5 zk$qRILVy=pJNpMvxfhn2Vl%EBKR;g;b}GJb zjrJ|58VCF-Jm2>Pah8HbOJTNIpJuo)-v^dmvz9QUO;4Dt@hT&aqgc-u#s`=HwlJbC zD9hZw%N5y+j^tjx0Dtkf;T*@7Jz4^1z4AGIgDO0NrkWDHT5Z? zK>lSJ!i?WJ0HWP9Dg2q{elb22C=);c!B?c+-eX8^AgSqQHY}PVja$QwpQTz{#2>S& zTJQUT_~;5?X02Z~D`&mjca1$h+>20+sS7>QJ$YM+!Q*qc{n^Wj9(`#(GCo8c7)Wk@ z3w$K{yn|yOB$-?1Q%T*6vfvymLSv^a@_CMOh#g$2lgcli;{CZLeg%DB3j` zXY2~Bt3clP$*h+eLkxjPlVd~mg}{v=-7P`(R}$X+fT(?CQ%^Q?QE31hyYUWrmt7=L zDX^4iZHL?OLSx6^7sru0$xDD2GDTOW$9z34u7i6*yFPQs0Y01pXCd z#9NqN3k43FoV0`VGOy+kF%g5o=H#PlXgDj4iwQBvFU#@nvh(x4SKg-UPvwWkZiWt$NF^#q>K{0a zAX)0zDRxgD)fv&M;BfeeQ5k-LN2Ig13};@H``IvUWXIYc)Z3J+UrU!|nciN>*Y|d~ zTQGFhG7`{!BmPqfg^$UN@v)8OlVo+H>@JJ)@IJ$^n4rAUI4LK+V>&Rt}B8SLNg zN2LJ%4~1-wS4Edgt_QVdi}<#fIQ#|B{in6lxt|=;OLPw38F4&QZFcp+3log-KJDK1 z@CH@G>h68Lh;`+lDQ5S~N3#!B=}%muKPk0dB1m;%ojua|(54aK3AeaQnpfByJAD+6 z!4oD|Yv~Or>wH|?Ap~5hg0~<>U?yob(rj#m+WRI%P~Z@9D`jh;H8K_18aYurBUt@DT1 zTdFTPn_U||h6cx5?^aOs+g%?l&h9U?!(*&j!EdjkTUl7oEyEQSD9kZ*1PE+Kvkl7g6#}?MLvmG?F&1fjpZplmeJnWXm&6~a3Nph&_p!Unaw>1b* zr(}!!S;4oSsi59ViJy0o+cjvd6#VgIQ)||V?He`Md~HHfaZArD4AuCl{Pg+f_{-eW zCWq(|_?BT@$?9-6F$^fv26IY+NI+E46v@qcaCg8EX(IF@R??jzpfr!YY%N5Q_^l70 z>}W%fVHo8~3%kQpPXx~rVH6xfT@h>nzM1jg$H;3skoEiO#wMJd*oKGekXB+$9DWnw zUUap+otT@EiYSG^np1N9WwhoAS#Yp(E(tv>K?r zwH;TbLYh)y&9coCZ_M~CMRf)~VQG@AP>Q#p8>W(og(AgK^)#R{C_i*o+2^X=mT=70 z(`b@G6Zt1NZ^(DmGa6I)mQYn~s`P;(;;Ih|=f*?xtoo-h+~T09&*dbbV7NkH8_(c? zq&5m5GQz?M*@8e2IQ21ccWO&tKok_!GKSNiusZygI0wA)VCE2A z6<5~L@M+wVP?OTu8DTRDdWjJJ`n#>DFh6w+ms?RvdO^M&hlUft6o#nb%B(F1<>nJq z<(s*irQnuSNQ1Po+Z5}XPi7b=Z_n_N?I4@DvO&`(^ocHS?QOuu2-9Nan3%|aWO!1` zkJ?{Wc3}@cW^Q3Ok&jOmu|B(JI{)f@54ZQ7XFl|`JT*%-NY)>I{EjbIm)i);Gkg+A z%o>Pc*KkeG^V~!`r$g}?0x7=zCNJ#j=PK2euyKEDRPb49dT~_G$7RM`@sJJ@t7S0YRbCo?+_`+0zV$OiA z#303}y3HB+Q!e~P08{-4Tc49{?8lvDVaaAw^q#P~#l==rsL0W-q=hpE9IuW?+J)i( z>Fx|=b56{!RqO3_mx{8+k7rd{oMdv#R|b`8o4H1c4)r0T1QM~L#6V-2j)7_ZX(d)& zXFIbs?Xh(cJCkMBM&UT4#Sxt@vml)_*Lrz;nwm?~xILVoIVweG#HoCC=`2+cc-V>M z#E-NISiYqXFrVnjbpLYrq*&m250c88j?RaCYKc&L6%VMqQC=LGil+yk53!7>44K2H!iM~sV}uK2v-K(HJc)z30lpDIH(@@ zK?KxdOwsM)o!W!|`5k&K5|ME;1=5`6Cr6?mOOI*4lSeIsh*_c_&ljzTOX?SzUw5#r zoqUmdu((3ddGVScoC8lPU)dQtTz8;|3PvlfjjBkZm_%HSRsY%#Ba?CJfQG|G{MlHd z=HInyf#&_b9dyeC-5+=Q@G!$?vt+adA{|rL{AA$>bUpdNW?{C~n7P^2QK5IU-Q+~f zcdat^)%w*)<7JT@jeYSGx0!r}5r%iS`W@q9IdO$OUK{svkOhAtG#s}^d*NiopCW|`4S;j62ue{lPA z8U1b=ZgUY?IRoAFz|c_@g;kgd?4Bb`JB|{~^iTxCh%nrAvLLXSdhml4zIClj#IV=o z2h>vr(MIC4i{5U;q*K}^WX5|}yP3`M(#8dd!_44k*#XymDyyfnOZIx(YV!;O@{z3_ zP&1vq-6;>e+pSpIK`CDtp;FQh^))G~XsT7$b9a8QAI=qwyff~a0OI30B{P2wp=?xBFrN1{lIKTbk!6Gu2#Sn zp5q-;vPg#x(eHH)+zQ)ET2l~=0=dU3;q20j~`~&Xi2+hBWi4$L{qp4T>QNyEu963>C%3gXT1M~-z3U}`xmW9hh!g+@(bDeGOCKD6I)B6xQgQ!@uR)l zcMr3%B+9qaMXwF097h(>^WN~gm`3z) zr9wZamL8IlmRDJbRUecHcQw@NOL9*qr;zBdY}p>kcBu=nrO656!p)3an=F~M#L{2# zfH6?xZU8sYWF?@ixZx=`*BR*=C%Y&&p;hb!cQ$xE$~t+-3u;y@2FkL!zvpQV_Z^Xe z&DH&E7iYvZwz0k7$BWpEw_}iKg5|*x9@kqaTZ_yr*M$e1V$`7ZV6beU8GnDRn76xx zPB`qdAxu@R7g~>N^=LcEKF@eO#t=h`uRA)yYj2qEtm;?-fJ z#ZY+pH?2}UzJZrlS84La_it^v#CBe8CO?G@89t`pWQdPlXLFf0bGfwcM8-q0)!_ae zPij9PVx6^4JJV-9>SrNfD>QbwgUdHQd_VR92(a&?#bZ9X8XTP>Z*z@V;CFbMoe*W` z-7ei;e{_XULZd@QmkS$pnnIiHYHg$0zP?kCD9I5_*m10^uv}yLz}regt99c8Fu#h>5Sa5XS- zuzlHcdO0%k88zkCtn=c1JLa^j7zh~MdKh_xT_7u9TlHx~_ck8ONPWJqsD{?7Uv@)# z^?AJ_qaRN6 z6Sg=gswP`T{`Ig^LmJKOuz*0ADpMti0s#k8w(-{vS|Kh`42#Sqz=kC+JFA;`_DAk@ zTgpvL47Ny?;-MwvC5Qkrd%^)xNCM>KE{Cq~e5h@>uVgaaTm{{R9^j*1-$vlpes3^0$|Arkw3f!C8m zbOFrITG}=W*^kK+7zQm;R-`IO-Ky~0SWxvMMPIqG6G0Y>xYDlu1FT1>4EncT$t7&F z31Pp(I)F8rEU1;EsV5LW+1cw*cL4?*lIY4#h2NW&0bGVwWQJmc)TLy+fLZhB+-gad z#SAy~XFg%&r|Fq0mE5u(o*KOkPwt)w62xE{_Udh91=q1h#fFHeuHUFsn1bm!=h`riU(f5HNtPBcL&g6vxgB=AU_?{8axx_`vh zo_w3a)1o5$wCqM-rWZSYS{AS-TNT)jppFvck29pm(7oH$GBU`Wpw_lkQ;Eq`r({)c zVq@fH@>~*<$rsDlS!h{WN*D)!q09`yOQx-jeP6u+nqfx|*u(Sxgd86S+c6J_<04?Y zsiz9z8Z+C0#o?RBUa5b{aBW}(de$M`9BP8t_p#_}IpsA#4e=m-U43~xrtBo`Dq@|V zK05;l)DPK{tq1lO1TdgO4ZEU(FoP>xBJ%a>X9ls4-9vW>=)78|lei^sLk+iSQNTPd zvfu277JJ3GvWZ{leSF_t2Ijub5Bl8Ak_kuu#uvacR$* zFCRZmI-0?Q9F)Xkjb_>NMLN+zF%piBOrqE>OQ=oE9-(mnt{Ta7A_E*5=+gYJi1);S zVBFXf9=Z(}Qj1ApyNyt!t|1);fK_Dqo$H>^Lo|Hi=>6B^du8WDdITXF1k7J$HRH`G zdJJ8>!TihNay?MT&nIwVia%JC+n|Smkcs!-O}lyXK6DevB=zv-DL^)>m&3b|`FJo= zk7{Ln`W)z6Q=5Qi&LEj#r)cr<&ATmyH=jOxG?74=83-rpnuZ?)q8J0mSMjY*hZAxE7Vm_3dwpSp8saH^R zRIwga$7s9?Icxjh(%ib&QGpn#YgLXxB~8r0v!`Kb4nmy5iEzG z^phjb1E3HO8$MIMIMWC(U)Fx&&(rIqFoM-;cf6cO*bfR6VVE9ZAJIQ|X9uAh5Rg06 zIsF)X*zA&YnMM(dDqQ8x1R$xtg1+X65cQzol8N+NcW~ zfbzg-u&*(de?z3a2bics?V3?~hJp@ZR8BGMQ02zqK%}f|zNHSrkb!p}o48t?>_AB{ zuvi!3q`T2Ckc(9=qni}9&0*Z4Yr=ZMyzwr6zY6dO_xIBCD&~yd^$1VD%KDH>>I}K2 zpq%#5d3d8}4Y{H?=c%TBXG+8tdW|i%ioRCoDYFQRE7F$Gc06)4k8f+w);U@^pN z&3kLrFe3LPvp#+rN$)$(iYJB1`!1tRoNr4`KA{Vrnn&(EVrO5r|7J}CM6x^=qq9=B zy##o*Y$BKA^yOUjAm3M6D+s9Q$GrTR@FASGVhzWgZO3lt?)3aK8!1POo}Es`{yQ7(26@-RTE)@OF69XEmi#Hk@ zo9!)57jzfrw`g2@NVaJT`xvksVojFLE9+kC8c~8(vag(A{n`!Qa5fv4e;(4?qJY;K zN;$!fJ&JE9daE9;Zpfi^GH(WVQoQ$p0&Fh}O5HoP7mlQtYzs5$aUhOJvRzy+&Fj_iNmiio!jmDilThVU$@f*>Bj#0236++8_Nyz}W5hr{wZ9zb6u{D!y z7Yg&m&DtI^9)G7=-x{6CBG>yUw~84R6Jf!JtJ4=G`{&dX#Qw^rxS+7NAnr8MNpC$? zZ)QU!3eZxXcE1*mhbja@a%9PTfDCASc+@d)9L?5mLp&?DHKYdv-uxmgLEViXDsSGI zF5g!lj=Oy@T}!SlF-7ilwQm-kZdfg)kAdB*^Dvz?Gx)XugE*RTyLh^JzUlt|7!K#< z#U}O_BQ3%4KO3}}wWE=xrJ0G5^>0d`)i2YC#>U=6-`dPw|G#O0|4kA6H9p(gxER?R z8U8m=@IRXxSnB^~4KlN^u>H3o@JRD4`j>w6O~V6HVhkX;{f`3fn4~YjYAk-MVHvOq zZn7LEKwLy+E=xFB+_+^N6VN-<&EJceZ_IzPpuRd;qf6~Z??+904F7Ei2YUYIdOHoA z*DaJ(6@^qhUb@)jMv7S5|HGM*X0a(XpbhMu*FFXNSj$Opg!dJwSctK`Ouyv4o=5t?vOn zO$2=acz9|n$hGCJSHU7=S0`|k3=Oq3oB0YAAEiUsmvL($sIjp!lm{iA;somKLUken zY{5Xl05M!KIGNe40hlsrk|ASIEot#K%( zDX&aR4-=+F!S6Dxw{T79UP=kgGzNi%5)D%a_5B^vmD&AL9mww^2m5?i8KB!YEY!>j z;v^PPA1ley`{-b*ri|Z3l%vr&@M0PhI8{2rzD_@2d}-vp@2+uvQ|d*=)TJd$W!v14 zzCyQq@Bo+pb-#u};Cy{N@8I3Q^1*7@GDpC!*=pR?k0XW;aNA$24z?0g4N8KFj#ZG~; z!}O{U4gxHHFd1d0FMILH&`?AQ2+S;q=^8K_ZG0kOm^OdzSo<#?sb1aPr4B?v+UXcS z$6M|bF0+3mN@n|^z=W?pB7(nJYzaF)dW|71$}Rzoj-**)noTK@6k5T3 zpDfyL{>vf3xY$n_6#z~k-w#F>vr1DC-up0!Bf-h(c15)c^~&iH(EZjKQg$ZX%s^fz{Z?^$DAQ2;M=#6^W19RDz0g}79up{ zZ!wn5LV&P1)O-9g*n$^H?(-&1kR^3wz5UOTBNI=%IxjTb$pB$Fc!WeRu|IS|{vRoE zDZ6w~Gyd>E6)!7>(YrIv3W|o)vX!%x#d$a}#7lxRqdupyyV92qrXej_fpt5~)o=I4 zSpU#M`D6pVN=HD-J`SpXCPK_HVzI^WF^1brop$dQgp zo%Z?(NYM|4yRDImh+qU%ND^?l<&7HNgw-H&u~_`@7@;G`sF_v8`y(rbX7 zsN3F@Vr);xEau!qA)bsez9IscdzlT`3HUR%*PRJ*#P}G2feE%FWI1wB4n;#Xvu#E$ z8}#7m3RlqkAH z$^_xWT1^V6?)^pv)aD1s{w;}X?oo-rhq4?!KhLsd@U5fa*!OE~+$m%!N{fw^T$p6S zkvhFeWqlg6urMMe_bDQ$PCo|<5TI8vsv1>^c}^b}fp4^v7Pg1I8!yYK#zYe zljhN?fp39o{QT6Mpm@6+Epo;s8K@T%gU#yBV8$MdwCZs@XBD zN`%m88(EZz!IcXVUc~y6redL{0N3;BdeNBv%pLEHIj3korfHx+C!nl=vF7t)%?>(2 zA9`UqWMkUqb(K2PL@%dergpIq3NUbyi%uh+KW>Wq}#%~Ue(|GM&EEKr)xHpj=Sa_ zOeks~>Ry<7u^Vu7*sO*PE*GI!nS~|a>URQebRJn_$)Efqy1{C@45$yKx9u0n_tgGI zMbMRdx*258MwS6XyD9CHUsxoFX^3(R**3c|y)^!7JJ)Y2%IY3(nAXWsg(g+2MgSYv zHho6AUsFjuZMqP2D*_R_W-C8cSiBGwqYTrAM!$K&1EBhg0b~#e& zO1U5|w#faX^x-X`Awkt!5PdQjAk-a3-MlX)SMjgK?6q!18FOQkT3MwYEPs*Nz#Phw z!f2ZS@7Y_I1SMFTC5K^)KfU&%z>6Mhj`SRfseDz373D1Lzcl3qEC=*yXMcyD+Y~{r zDi_f+*Z|OCtUddWfErz{tR}6-0KN*us>J3-*3)e^K^*|TB*jO6Rx}MM1U1%SAv!c5~4JM6e32S#{(6_8JvQ+Evko_db$F zz~Q3XbWv@VXQD{WOt;U`V7}5u$R94@pRz^!a2?iWmGgfao-*hGNnX8v9C**mqiqf{ z%T+sn#7#sifSC(}Sh=xsEF;S=lc*ld`}i?WVkFg)bEgC=wZ>H?DT(KQ{lhzjTZEF7 zSQ;;XY2q!ND=9J5FxwUs#PyT7)bJ~mtcp*^&H~7EWe_Tq2jT#|)Bhh7%ITf}zMd+c*W8+0cko4aj!3ncmQ_wuEU)Ujz{2SI7B!mGl-LB-;msyiw3UIba53w_X%DniEm6OV@m>6_w=}0xVd1{1 ze);k$VRh!>R-UTqsm^JHZ?1ME>z4BD^ge#CJ#gv@!G4zCX@G&_%)|@l{*X~(dnhz@ zzJ23Q|0|F=2tmp`#X!N->z0|^8fqp!kSi6evYDHckVK`ed8D?4E6qCv+_?%S50W<;87&ez^m@^z;Aww`EK~*-p?Uz}y0oZnvBzM7F`7 zc*`2XS{JB99a%YZkKT^vxl+vT%z8Z`wT>Ki9-pL-MP(M~DzPo4p~bOHTw#O`DrWeE zW{U}CvA-vL{~-|(QE5>bfPmEK5*Ngfgf^i(5(qO`xzEL*(d4!{edgYRSr-68aNI%b z7-WDL97)PRWpBQVFP|EMB9QI*hsNdW=jvKq9<=uW(uDb6YAoe2hat3(Ix2x2Zn5Xf zbx)Uuu&h?rx7Hm`6J8f=X^M4p4lV39Brsn0tJXUV+nsZ(`1T)vjJcQ$)B#=kUV_>U|v98-00Du zUcYu!HE61rsPihX0`GH~AJ+)-v#{rk4oycp*^~Cl0|deMxNeem78`V{j%3YF7yxZu zJ8nd{NGnVMO%GTmQLlOi0aj|;+!7YjNwEIvp2ngyecR2qwSNTYzSyQ?mIAyvxv+EI z6Ng&gucJ;h(?%Y_<{3*3&sQrvFF;;*s*lrEE9+VwT*2M^8Lc}eNRKf-xop;LsIC^7 z*Om77`!@kq%}}k^=Mn*DkMmbwCC+zg7MnX3XWg$C*-0~jk9bm&_CMbR4sY9EC$AN# zcmUVicGX8(qJOaPj5i0?rbkGt@mR2Vwi~-$OSnRzWi^aWf(k~$4N_h#pmnnZdaiVq zp~(<^@Zaj-B)0@Z7_iqqRtI9?fFP;?8Qs4;iI~B1f_7lXt=~L}xI-jsHvoTwP7|b# z+tK`60$N7yqkx+~F29`Bo^^b-z$cz$&1^y}dn7LM{*j$cNwLRuCU{BE6@s`)fHaxl}J-MM{oTX2?dqX2L&l{b-> z9t#L}?_k$x1|9?yKX)9GF77|#N_~iIzkaqHJW9Ic*X*nIa2)Ef7@QBQPn~C@k7Ka} zI{N%>oNf863;1e@J1+cWpw%w6C7f^C z$a0(J`5b|}!=6yQo$ zqA~d&U%|oD$jI?m>|kX6pV)E5b7j9FX5VvFl6u=onsgNVN(eYxV%ZsgwNyp(Cgp}B zx;Q4Jhu{APkk%MqeXjhRQu!^V{BtyiI7#VjDw; z&++&HVtvDXNhHQ)R_KvBJQ^rG7)gR7!IUtiei#5}v&ZFT|GUd$O)lKR)NAF;VC9+H z5I%Z!2Zt;$K8Oz<3rAmRSskHbpGogRDMIw&*u#V zH(w6K>nvm)I&KU^K^6rB65{2^_Z>r&;;j0h=d~ySd?<^s8IjN#PO%j+sp7a9^r5a6 zYO7B!LxR`h$IEqCFr!I&nOSuih1x=C6wCysfk)-W>a5gOL2}WPRLSt#c4Tl(e&7PS zl{C009bpP6i}g2aW50Eu0)M}OiQs1~l2o3o-#qXv@YefTw$!w4D~swA#LH(7WGt_Z-sEM~x?&4d25qBDa%U^!;D+84Q@TCX z3sj)mKjqXO%1j?th3exd{dAH{D77aGy<0KC82fz07dJBzZ2kFhmkr$plk?13IMPna(cT1aGagu1Z*6I>iXR3_f*;WF&aslLq z*uoGwB-mE@VBYMKt|Jcy!=gR(Bz^F96}iso@B+;5bW<~C6|6k(k6f) zf)a2XU-)|8^qU)3^tc~;bvIcph8M#H^6Zcu+RTTJ!HaVk;kA!S`tyUsM+J;> z21S`p4zlM2v7&(SKAng2(5Y~n1HMG`4Ypo~#&KbcoiHYyLmoIL z?Wdf~I+c5S`jF<38fHEc$Ib`310sOG1SsNz&$q)K0%A756tPFBxJ4w%<1%k#+D*;G z5kErj-MPOb`E+ng9RpKB7+xjJgOVhaUeg~_GfzFtr4jqWEF*bC?Oy^rbrMSxGDhY= zLPP?qG(=uTQ1u4F83K+LDUe^~K~-em=}&2n#LPEaR9jpPY?d$JFsDA}z>G4~^{r$y zgr1c|WTGsZ2o()@kRLs8;6TD}#AqBigt9>H(Lnho771+EZbSn;8I6j!GR z%s&mx>*dQMXEMndrwGc<<=d?zq{FjvK#IadbYA3#!z+e4D@?S4Saihw&$aMeo1BEE zITYpvA`P5YQes*fZ_H34J%qoOl8dM=9ue&I1s5cYc?Ike40t#Rbl}XgJI9@a!(N;M zyF|G=sZ|_D=O_mH%XO9m1+D+)Ur3`7>o{<+Mo4c@jf0hJte<~QtHL9@@?jqHe2lv9 zWxFj)+$8V0uo{ZJU^|y@`AD$1P_2=!*95Scf!-B`$SjR6%3#z$?l`x7{m{brm?Q(t zZ6zg?GFW%LY_iiBghjiD7iwIEI z5R-&pMkKFV>lI)$<)Z+0J?9lVYku{`GbD62AecIgVwX@Wk`jWD0p-`%2Y~~jjVuxx z%>7&joc0dQ4Yz&=`48Qm2w4PMx=*ul4x*@G^Ak{19lDz8F^f7G)O9gB6CM_eYPdih z3_Gco8JB~#MR4?JKhcrGm++9lw8X;v1KObK0K2K+EkG0-IZp@fgbs!B}fK zeMJ7bxRVfKN6%DgfqXsYCsM(79c19SG~0Z5*M_z?;oeVx_9D3GenZaUJA4NT;TYp) zqwT14#4Pmd#9(V=JU=0<+$vK}Amw1)Q4?yvyoqxqT!_y*03MX&C~%>TjV&~~cAK4U zFREVSIcyr=vm@O2m_EM3332efj2j3P#66i{epvYn+J0C*>Zp|V6blAOBc6B>Md6GH z#Z^>&dJciVt(PLazo&)7rw7!S(DoOw?a8urN9G5Y7wa18^`9s+|BTlvpDPL1!$7$o zZ`8lPVN3{9S5gb0_I9}Q=!*ty=g_mTB@6RFjYulRAOx$o(6irx`Cn`{g*~8E4;(@b zp>{JrzJ&CZs8`r7>j1>xAuaiem=kbAT(L@e`XrE(6+r*ud0eF-6hBr*T?A zVzwgF7?=x|eDMlL3de)rQnbhZA@d2WkY^N8Xpz7h>7&xvxr9Fri%dhbgdg0TIAZ0V zseHA^{Ir@|*!WSSg)+G>{?;*)M#Ocl;0XyH#jN`$OgjX-s9{Pd;Auo>pT-QQkpR*E z!f4_fu>58HgJIYGj>y0jL`HTU3!yhSTC5%|TVuM-fs%DNg&BHjHzYJ1e$VCo(1Ie(iCK*Md3tNf$XN z$d!G*A9~FFb&|)4R>UwCS=IF=Fk+xH19#Hys- zfY}P-w=5|ihHQMFgM`K3{6(ioNkFn{Z^g_#)b#_^jjWLgPEzmuv?NT&eiave(~#gy z?1f3p%bNAlpC!;$-axd>V29*%jUb^vWDva&0_g&wLZ!a=JUAINxE`)A1~*bB^3WG6 z09F%SVF}Yt?dvHj+G>9yhH(GnknfTElkhgw&R`y5e~Ij8Fk7X9(l@ZDPrrJ%huM~$ zggk~XYK&-frZBu$=gU`eH2XzmuMITMuhh<6v>B`@l^tj_bxl%9Y+hp_Xz)D&rGsK^ z@N%_r&Y&vphg;8D^i_*=5c+rt=QHFAVanB2{n}K=a=8i#cGmn%qjE>MnsQEIaF}uy zt}`HPZ-+xh%QA+z`3#HvuA%Is=SwO-LJTE%LznC`Ui&!yqN!skxQj~Mh$%MewCt#I zF|BAa^q9SrT6q|NASwS2-e!55m8)X3Cv3T`D%pdpR*G{G8%UDF-xT-pwdqmi;QYMNx z8Vq4KBL&U>o$v^3(;ny=HjAS%HJSNyeuvj<1{f9fSto>!GY>Du6-0_l#%zO(I6>rd z3N8>jP_$%X4Cg34;w`2SM5ZtATq@@oyFYb0StujDHTPKO=Y$2xU2JX;k&1bJmi$lt ze6IW7F|vsR_3$_Y%ER6pV2K?W(7D)*mLnah1gO$ca`+rw7oC;`d_ zhi~N9fjD40eoJEBOx=nR)AC4hMF*&})X;G7zPx2xaxl-#63mjq@F3<7reOoKPo|{U zw}-2Amc#|MRP57-@eGRxCM%0}UvTwH?bRbqrDn19i}X}}v1*NyKZ19`^_XM|yNFVO%D%d_kSMa+7LlG+or zHjI!zz2uMaa>QZsa=wjXSWyd>O_j$|5HVkHrbs-|2KL*4fGxCg`~wEO7`b+$-ylZt zj%y-S1+4dMv&TERKtFP%o*AQKTj)IXZT>_dwm~5rD26e z{+$&uta0z)Z?jIBE>C$D>AW?O`LwtsGmVndnV|wW5!jug=w%`KvGlyRM=>_B{-9!B zi6)aILEaL3Q2k_CWq8f?&wm6T!Afi5*0XJRCxbONGoPpVeTSh<59B$-b<4>Ly_ov2 zaSc83k}187vQ{CiJ^YUU2O5EEKJS?jlFSrmtTY%;R_{ zqL3mmbwzgh>{Foup}Ha8F%ZH#3l5}1St<$(4gg&L5`doUUJI5cNE%cmi_QMxsHR$t-BStWPhtMn^=9@8sNG0u#)IOG z)k7f3praIe5nB_ZG}cRii8`D+Oa%qeC`k}<`2R(}9ry~436aKBU{w8McI{3&sY{M@ znFX#b==6Ai_j-USXDQHdXh>6L>HDfp0Jh3^fur@^l6>6FAM8KsK!%?z6O^;&nVtVy z0POq8)%&=;0MqXI1(C3v##3E5&mF8m_Xl>GCKwS00s@F2PLdK!6*E6)QKzImfw}i= zSB8^4Fx^WDx|Qum|{7f$C3szn73}{fuW&z`rnkFAgbdMP&&sO2CW*2e3vSi zOP|G^dq48<;4LygWt`tQo=Y6tfsa5R>vMug0GP7Q0kCA0#TXMhcp{4g)bk?09dang zNsGEE`kfJr|4=G;2MwFdD8$;Y*8osv7wa7z>H?~8OZSBj>H;S6ND3IP*BCa9M4AMy zr0=$*%Gt)fsRP=^YwQspoKm~QCj|r+hzz0hH>d(=x;B>}ho8`a14V3+W-_w{&ioc3 z-~5BeOWna>C~APY2@e8aiHj`}L9E>d_xDK8kE~U2IsZ_?7FrAEo2snyh%mma(HzK)v|2Em%rzS+>WdXixVYd~{yiKa!Pw4QdE?ug#aJrB#;&}hGL#J}xBaT*_TFOldmhmXA2uPnv;P*$z@D~OMP8U6Y zHAbyupD0lAOa!UAt7zCT;+juPZN@XzU^g+~dr)G(^M6t|sVo;l&uqpkX(ltau9qum zq%nrh_bX{+-nW%CAD?8MbE^8{Kf;)Qf9CuI7Ji3y%4(vMO+cc-uR;MOI!y7;wN?(Z zOoW0CMJD1+AbcP6{M`6_7e%5)A<@pYBNz$i8Dcp1?tH%We78xJ_7d17ThL6!gX4nr z$@jg_L8aCi&9_Qvo3!JV5Blkb!}LRTiUDo=ojMmN4J)|U0zh@b(T=Cm?IbPHg{qBN z*3cisSQ+;a3uzPz<3gZo=W{MiHOvXr7{bGuxcGCgBkHSTZkNp9ZtZ^(uuxAe)o$IW zuhxDTNNG(CWEW(3$+B4vX{PE-g=@j<>ttnfq25{C;Y#sLn3t~n zmTjA-Y}>YN+qP}nw(V24b;`EwuKVuaco99>xicaM8Ihy)E#VMeY}y~5H#Apb?13B@ zDeU#R!bzHeq0Ki1vQL(+QnnNP6RyD@a%hjSf~5S^28|#mLC*??fpt#=*AD|v5#A_J zPF4V>Hdyj^J}U?O@$s>VTaY#GHM*$GZTVCby1c5puvfkT7!7BgBcHND^1w{uK;|S- zLs3jdSq-5V} z-rQw)ktDPFsqXey(I?~$rJ4r+K~_EZ^-if?1@}Z&Js|v9sb29ve-$HQd4Uxs#M_Ww zN9K=8^lk3lTm~}NvAm#4PYKtirM#G2ziV&~I^z*BvU$xYoSZm8lBt!Z2Abwp;#=0d zOBoTxeOLzeD+%N=>C%>rGV#dfGf&(IN+oYi-p`#MU!8O<$(gMoVFH3X9>a={=;tju zLD&U(C$`gF#uVs{=*39qMdk>So<#}%^edNG8&R28Mjc#;RB;q(XQ=frZ>Ss z97=4>r%z;13}?ErKK5NXz3Wp6DHPE z*R3%Fi!-QN`SV81ZbwpceVw){-qlt&m3!+_(xj85t%?r(n@5=5l5QE4;NwFCnP6D;6nmn-vy7h z{a;=!FNkQ{-Kw>M*F3VX!dhQots(5H?Ge+bk7%j-6}9f6Wri(cD%Rz-F%f5H?w6RY z8>`Y=3g>mAQ@Hn9bs$!QPsUU`lg|t=ifW#jCYYgjh|SkGeOnvf&*q+@$`5zReh_8N z=<0O5*?bP$-Y?IacyK#E4RWuOe4krG6w-`jyOJ#%iNid6kp)oHp+mE}*EKZ1V$zJE}rS- znq9i>ZZZljcuSMp>?Gxmp|-$ZF7NDIWeOav_3TpedpDhkmD+jgvRx58vJ)0-+tOu zkAGs4AN(RrKG->>cECD_#YDB+b?=@R)WvsC>ONE0?((Xj(!-h0^eX-RDuBK#tNd$i zo|$FN45pP?V9+ZJwazrw&b4?J7pX1NC2Rcacae&!svxAUJD4svyi+Y|ysoW>nLR7R zaKW!dFFQy{BliqcmU$+dU{_UZ5{Y5zBq_FETf1;?D%(KzY*?wcO_A9u!r^bdn>TJe zvF1m3f_8ghU)~X=Mq8KF9A;Nk7L+0?o-atY3BBj`yPG*_bCdzq1FL+;VXx_z>beK# zB7hvOiZf&{Gj1H!W!^Irc{AkO?+|qfTN}mfiy%|z6uz(BAZqVI6!k3AWk|1800MwWFehq2OSRb0){^$` zk8M0A`2rOczBQjFuga(-r|92#Rv(z9CFF)fZLQXbhJruTX+qLFi)MBFwGz`?Lx-xD zNmR$gw5ap;MKs+RoKtUA>VR3xtu%ZhT`Hd&=9Na>H!m>Vr8gaBm0U{dmTv@TC4}^Z zuqdTD7~k6z2y6m|X%Od>kU(zNv76Bg$?QcgB7qdWS}(9_BY8^73N*9H{^G{e^y7K~&U3c}$5b5g9Qg98iJ#3qIgx@=W{)6V-02VrHW>s1(DyVly}x zdSRX(E%SO_R+}ZP=A1I)Bi)Fg3n)L?)J8<&#%8)DfJYnT~&RWTW2UP~w})7Mjb1`XpFfv|W$D>9Xv>)jzL z`!bA&W*@Rdv6BY((pL7!Qffc!gl1bRVCGv%BJCz3RTn}FZL8@)X?tM}6YtoE0`jY| zC>d1lOX{S&|2X>z7M2`SHFPb9na)$tiO7Gy+OhgKznLRKh1hi1edWna!?>`F`|ndk8ec$> zh3+kb#o~@_fT=Lt4@9p+dW>NjYlYYzZclPZ_f8}$WvKnIiTXm~{{3d8PWZq@@1}#L z;RUn@R&@A;>!Df6=r}CnPItSn`{(C>vs0>yojHlH008z(008X&FFWN#W9n#ULu2+o znp|f)8e>Nb*Z=kt{cl=|iJpP=e>AwuoF7UX;+cGddh=yHl~eM~H7`N`Ht~c)t!;(I zfX0_}vn5+P`POs9x3DTe+Z8`=v}({Btzqz7txOA`E`5D(%=~tCnAn+O;~kRpN1MG5 z?EPjD{s8kli)u^B76?laGCgtz z?Cfk@#Ioruh7A2-hvG)=N)WTh5GITjJRT`UM`Ut-1oV1o)1#E_qnYQb$j?!j?9N3j z=zkJnCnb@d@Z8hap_Qs_N5O)-3^^wJTWjNMOC}+}C@*hLZ-wv@4SM?~h$;5#lN&S* zmn`5vWjK)Y6HLpm^d_yAcz~oaQOOSq%KQTk&O!qRr)jx#$idFv3;-tRkeJG35a$L* z!KQl|jh1&p0I~XVh~pDfQt+D`P&O_`J^`j<)t7^@zzf421riYg35ebmaB(3A=&FAD zCVT4XATHKy=kB9&`-TH$#r1aLY18G54D7wiExe-op2j_?@B;?Jczg&;a6M2VS-!ob9ccIg@FtbTh6)tIKj z3g6Z3c5Z@%Dl$-QfqJfk0tNlQ@n5^v$=A2kWEo)4&0Vd8o3D072v2w0@xL#yDGi}j zz#uTLd5OLS^)A;xcu6rp`5ysx=ALkePh zfUznGz6$ryi|?N#2^1HHUUuQm6;QOtK@nDos!!1=*`JPoeQ9KOxcX4^`g%*;zx~di zqwO8fOQLS=9Gm8vA)EvjBL_M(`5h(*O@unO#&pc~ZTD%TE)fFB`wdPt)<_znh^Zp! zO>$2VS3!X0!@KYuYZylMw?#FZ{)Wo3J@4n|>tRxz6&5>1 z0`Z(=Jek@aG~cHtA04VyX1}~Za2$v-sV1Co4Ww8RqADV(DIB9sW%d`e-ixK?rH#vX zxvg7|tUsLcqHs&fLMC(|fr>F5@Zb=UW0I=Gp*-YL$LoCZW@p}y?NvtHj7%+u>y;!w15#Ej;esyAIJ1EafG zCtl}(ESZZ^8Loe8uwvFMt)S(G9<;KMa$$~4ib`&Vc8W?HrV;oYaHOQL^->hObB)@U z9I-Z>gbmu9vGKJ0euA2}_z&|h<<|7$wQu2!=h)lzfN#Ct6sqz#?8AVBUKG_OXI2!$ z!A#TFVcySQ2q-;ZDjdX>tLT7i_~(GG6H4lctN6Oix@W_2$XA_YPa!6#exQ|eTU+Sc z!A*LPnuKG(EQ7(p=7E@)_J5#&+9+jk}CdDa6dgJyUAL)f3G9K z+Alitp8~CcCaYBPR(qDcccv1w*jZ*Pwhmnm5OdC7-Dn8p@AW^>yK{f91^!b_w^@81 zk>s9RlWslz1h+(Zr6pV{N@q`v(&2k{z^TU|TQc_D_as#xFEu`cK%r4mtGAbdMv=(8M1xAea?Og>`2%AUi zY(8pecS6Q|AqSmtwv5eDl|$M37xy6>^o|<6 zNzqZexeQ}^R=FQ4L57|aTgjnpS_t$c<|?;486;eDtrix$+4HL0y~g8?zt^UHOK-WZ z<3dw*Zj@ucfe|6w#qEv!^rnqtgLoi_u12OtNJ#h_stEsj zC#%UZ)y`>E6cl1eg-%i?s6w`r3NNV~;uy78vNCyI%xnz(iq~&!jrvxmr8@niPpSGY zqU}~t6B1(A%{*x`m*vl9nTjw)+`@{6iLT#06zuzY_}(_BpYPg7cbx_J*0#He6}N-F<3f-dV)gLjbW%_okh09Wr%uW23 zRV!`2S*PSYF#K8EMwIu?6utoW>vut>s4Xa`Dm{Xr*O6Ivl5zNu)MW*+M!!PYO!yb? zdL~s7V<=U%gJC(t;bjpk36-eYY5X>o?frcm^y>LG874weH^b*^eEl;Srx!;jtNX*_ z@#UlrR@OIHmy*;=tT*fHwT|%=N~+E*t&QWA z5TsMt)DB^t&F+98!s|afBI`28>iZ{R;~!h=XQ`bv?>@hY0Zu`FN&@yw@=ER8BWWpm zaIBd~tJQ`9*Un3Z&upH1>5*oE-FgA4Gqh^g%nXPRIbe^Em+Cn|fs`VjrGaJEy(Y2Q zev<600x2~ApVSmVzJx@j9>kne@|y>EpAyLhMmmz<-|_qnwe+ zJCeeZI{w;3k|3Q4!)(Ilgp){!$_NbLe~_0b2qec9RV#(rv}u?dqbP}iEUY7~XlxXv z=azvwrTkgk&Wl{!z=r)LePR>C^Q{!^!Dh%x~X%15}|G8S4woB~{`2-*Ek1B`&k8}pRlq_GwV zd+g8U77gt8#CF<(q#jY*b`}sD$H;=qGnHtErcz9DWCSz>R8N7a?QU9yo@PyeM7248 zQuFjp8$r4nX6=P>@WX7AgYlY>1V=H~9dgSiH5s^KmJNLsp0>tElN#S|HkiHWq5Xn2 zp=`0Cx{p`orFv-3%I-y@Y&XCJx-(v#KEC|sY*L9tj*kq=IIOU-8x~IEku1Q$VTR); zvc{#mB_5qnY5E0Oxxy}5j**p*`91zpEQE{>ief7ftorXKhub}IW!_`YcKHVTYq1El z=oX}KJKp*r+~O6K&Tw2t)hKgZ>~(SV_@pyM0xyLsi=7_n{oH+@M77y%e_e&G4?g+$ zJSNLzU9>vOuUJ(tTEVd~9Ux3c$I0ib)hHw}6}1*H%|8^S=}yZyySx$zV zw=66*XLNOdQp|!^d>Pq+S5Mo4Bjp8*atCHile_ke4H@jeuSrZ#S{l%q0=m*jI#`n+mKq$sHHf8~#Nuyd zM>(%o)zzl6@ggJ)Y?RiZoQ#7>{-mz5D@TsJz(j#tskHMPyyt@Ha2giJm&fUFZ=TSD zk&tKy&e`h>l3Q4wN+`~}$Lp18DYwvJzgvQ)WcW7cpO;U?5BsEZgS-|fhT4d=S};Nb zx8Pig21G!n``cgzqS70VP-Fhlk%3awTh4p5=Vh1FXg)=xow&9c@tNQ#oIB{r0!085 zc{>xKno$qB-GA^ADwCycp?wT9RjG8Bg<_e0rK$f87!$In^T*yyx*h)$Ub5sc9>lzR z#8u%tcMoyy_j$!rdk8{N)` z)OnQF!1=IVv%+K&s+)Mv`=M_#G+3JtMb~hf=nG!6z%S3~E>|JM3*VT6w2cdDJnd>vrG958405d8U@IDD^M^0H(D6bzWMV7#P$1`Yufz|7*2$@^ErC z`2~Fc6&>0b*#CxT*!>?*rjwDQfxXH9Q78VM_${DiXZau0x237&gwvMdOX>%@@JyO4 z9PcBrQ!4p9(Tp^iK#fZ0ilpFB5U|J;?>UzPmPwpxn1cW8gP%xw!+Uk`9ir?j``v#n zCtfhm^Fqi{*t)OT7aby+$D4NJeX{GX*2NXOI|IOxgcBx!#QE{ND>kCHk_&KT27zQ* zE=(5Jp@cmEe3#LHgXW(#FNvo`jIJP$H)N^F7ey`*`-)9M^jC%0RyC=qIZX_51Uo&t zo|##`o{|0e^>}zWMVM3odYPWg#1_$2slsN6Z}x^URqpoNew8lGT~$evMEip4Drk-uVxc#@-tkjNgGt8P?2DU0A%9#xJA+mp%)^rJeCh$xY?=@3W(DxG z%tzBd>;ZJX#+f%1zmOUduqfwyL1wWJMnq?!w;6s8)vA)d{8uNaA`jMsXf`TWbDw6S zL@aYbXs(<*6*%kdU8x8Si(GzzsoWc)se$6&wG|`-@o?i`pD&`m+lT%)5jFVjnl$+9 zhzy#}&%ZZ|1JecJG<$V#Y9tVqza-PcgqM}ZzZ=;3$M+>tr^gT9M~3bcvPz^x0anO8 z%8{@`u$deUgn$rTf4HLbWPuIIj|K|X<)=B#3+CV+2nERn7F1il0+DW1Sv=%9^lAlI zHTGp*-l7t8Lo9!NpvMUlBjMTP(L@O~Pyx1PXfy-^+TuIJ{U?7_AxFwk*PC*rFyvN& z!bJxS%0#&dQE?yKTuDkOO1D|;?Lzd-wWd;PyLsJnmgpWsSxKE?q>ScJA%l3rSttq> z9y?pfcYUJq5`6Kq512ZLtDo_kFX*UGLmdp*nX$AbH}Jujwu~!2P>_G!Jd&8OqaC5U zK*Ygft+H|ts901^H@}*E9h)!=cbQNisfU3WA{`qxeDe3mOzs& z{XLywpzEc*Y7*01n20`750@m!nh&$zNi8ahg~XwFgc09DRaRK<%U&6R7L9g%wTP%r z|FYAhN~8(Q7@MFuS2(N>84kv^M~g6OE&@viqIehQ;Va`p=jD^!ox;@;t=H+Ofzcf zwVOeaibg*GtBl`vo~mnQycrg;|A_g((^f3o?$Kt>fRbEe&)VtxD@Z=g8*I^2uRdy9~*^RUyh@ z`2LN52J}YcE}8Z@S4^Zd*g9F+n@Ekpq8^z_k%LnY^mmT^5`Pt^)CIrB%6P+iw_jqe z+=%1DQ43{s9$)Z(4p5SFS*`tk?)dCPztbl|(aKsd;0koXefO`6p89wGR;P~+1z{}G zk1;rCL^pApuE|gkDjYlR$fZqbMtn`1E?qsRY(1nFXVEmQYp9x5vDg@`eY!l0M0fo$ zfh6^XNDtoK4^Bc5Oa3F#|CwDppXehcvXAD;=2WgVQ$4Y>SKer2aAef^d-V^16B8p+W3Bb$%Vc*}F z`cW~_L!zEEy$#2RO@xU#b=Ta6c?$;!DGV{zA0%Da)<5ablRZXKdMPuMPFiiuF>6AU zc=}PmDE&B4sDv!Wj3V1$!pZT}Ny5d5_4ua6v}as$_LU|*mjv2WFAZ+jF#)>#?c#kg zIyZkT4VBhPJci=&2g9K6u2=di#W3?2u(Ue=0lN|6WSUU-8uz9(JCz6gvtp}F;rdYL z^({uIjDVFF*3M~`Mqu5;w3Ul-uR#fAOmH<;3|^t!#t+AsNX_GRPta;*GWhc!vzK++ z6GBdJi97hXTYT+oJntVm_}eP*AI`QwJsx&GzO|1}^g1exUN0Tk9Tmq{Tk?B1?{hr+Xg_rLC~%?zzf zTiHelDWQx0RxuCk?HMz2q{mRfjW#QWx$?V=U7}nG@QFA*tA$tStT|Z9t5Bd$rLNRo zvHvzajy8eg`cM02byXG{D$RnW+)eILVtIo?^{EQ$|mX@EpNcT7U`0Kp_> zc(E5%3I)y-qd4KVf3Wtkp1Skg$oGbLXc@jWJ-+z+a(fQt^Y2&^e&^yMYu~6WDe6u` zW5eNoq5Z4f%UJlZS$>k?o&F_hOo|{@Xo_d?512uClmC1AUAU;mh#`6+$x9s`on5Tp z_Vvyhv#pMu21SRu>v5!)TXqylBdZnY;k%Y5b4~7ybA|#a89|ICx(}2zeyeYB$ZoUJ zra~v0G?&!)z=VS-X$YFGvZL9vvFt%w&ZdTUmnlnRQh3NQwIDgEt)xs^_CfH7!Fdz4 zEM$hIH4Lj#>a&)TL=!@D*yH-OuU8pgcaBl)a|5O<%AA*JEMVy|4VPW$Q*Ezo!S?=I zw7hAoZt!`PR{nWin5DEJoj(SBOos(b4~m&Dz}geRNyMxo9!;K^YS<7y#Ik#q(Z3(N zY|{=YAvPF$ED>5#gDHC~Qao8=q%wh25$@xg7^R{D99xh|>ZgPr7{!?nV8;+|HN=WF zh!N+=$uHQ}(%chMiuSb!is3)TrDg!=W&IX^x!GcLUe`A)u_Lr>rkj|fq?3|GnTP&F zYfCfD4r>TXq%MpV`943ixwu#51Ny)@ceA&kooj>Z2@|BDFKuK7W*O@ z^3j9WXDdlFtww)!``2^d3H5689gohL*HzCO3$vu}?U=0RL#Di=x2fAC&CZmrYFzHv zGEhaCcFPOXa$-nJiL=?Q$)~2h_8s(2!@xT&24l}(+ZR*@>e8wZEskWWKlCc)`03Xj ztK~Tt_MUO~6#|E4vxfMZ%EngNF;VvUGl70KU%FM}8RvHpQ3eg`IHsLa|C?3jaW}pU|u-bcqbBv-r z7CgSo=ZSw5NRB_P7QLS-(VFn;%X(WF=jI=CR8&%Dv-WI1HjQ)6s7dPIq#N8|b7W)R zkYgx_UAmb~*w%-Ax0;=Db2SMURR}q{UBJe+at{1i-SnQ83$mW=N{0S@YCe~Jg?jAZ9S?7m3Nt(i78v=hP5hMo-JG#%?*D}eFvRSj!vvR zb+@NT-uMA@7(sSE#VNExUxaQX!K*s9EiWqMZ}b6OdqKEES2O4gKV%AV&2-*o+uD&B zyAiv&^mz4r!2fSdq!F}P?T-WiaL4@rU?NA`-;fz&6B^t9d@K!I4SunYg}w8Cjade^ zziEQbzlh1m`9Eyff9mXw;U?p6<$+&@@Q3aRg@YMDfKSEwjX$cN%t2jU@$2-K7jz27M}1V-2Jo2ctkyJG6A7t z15wx3*1BzDXOprsEK7I#4%Y;h(`x3iC2RiXyVUgVfCIyg{{eiQV&nfcc#u3)s2dY%QrlS{8WRmm3Yz#%*JZ&p;Oh&zo&P!&~?Yn zNu+H>W*UW}rBhdPd?A@;wKlKm4-=cKULW?VVJ_uY_OydGcC1xVa2R`S%ScObjSg?F z6Ax1waPIlKQR&0Q)j)qkcJ1Lhw%h zMv<~^IQC&j>|!g$YTh_mp?A~QU2Rq9UHoAvAyVfc_S~bFF zo>EHq!q_Vt^tP1)j`J4{E-d=0#T$mni&h~kgfQHytrzv#h@jpbGwi#|fA-zt?V;!y zp=wpAeJrJCd+6&j0b6c4vk;f*Mo2kh2a2|+?rm5*z2lEF{-CyNQ8lhy!jprnn$_sf zHL)9U?>c;~=LnV-q`Kh(HZGowVwf@j)ewBF;xM2oCj4Svds-a6rqHtc_yQ_pY?fs{;B ztG(G~5`zP=T@CJ!xDt)kEbJkNQy771$wW=RO;Nw=p@HbieGRv`b+;_Mb$F*rU(-kL z+@M^igIeR!pTN|qD4tQCAoGmA&ZV&LsW{Yf19e_w&oZWgROobx9O`7BbB!hLy^Jf~ zRK4chuu?!71-KFx--{uzXiGo59d@e?kUiz0h16dsTV$D+>8MAH&}hDKuxUYvd+)FOt&tUEt~VTL(%f#uE54A zP;pg%Eay!`C7UQ>hmTg<$a1}u_F)a<7)HgYY~73|oi=nPUm(Okf&}or6zHN4VumjM z)sH69OPM!_o}@1%Mwh;NjGy>%zwst(D*lmjzw@@+DV{oAXKE_9B(76WOh-{fD08&^ z>=nPuo|FbWQwLQ`OeeBjh{~;FvlXWz^_MB5kd?7bxv{L!U}T)vh-^@bZbZ0nNtxCX z4FJ3RCWVh6;#Ou2tIL-(%#yjgXlS<{&^$e+=p$n)SPdZ~FZFDdjP@pRgrzibRlU)b z3$es3y0=NvJsj^wNq?b*PTNj~hapc$(W)5?mF}A2`LI|89-7XnfnOB9m-Yf$!eb~Y~rhP0Qt`>EPpr3&fS z4;=?m3THx0+-uHvat$q>8yFu)EPi#vq_oj}2gE0ZlmG*Bh%{$jMk>X80xup%5ecS< z*CLMhAYmIuGq1Un>|iFe&bH{0AI!mR-F5vY{Q>@`?JER-qboEs{A{ulZaQnC+=2_VB*?!>u*sY{@r7 zD&@oOKc|b~frn4D_U$lBA#&u5mC!jF$^p>0$7)s(N~*UM%=@$GPLEj{x)4Qkl+wYU z{V2L#|Gb|^y`P7jAD~gPqBOFw*}T8=QG+LBgHLyvM7zeJP_QGEft#RGh{mb9HAtQ5 z&oV3)8vxVChv&t-7OY|QX+(?S^Lh3q0wbXG1IR;v%a7=H$RD2N68)tooMK=kEOKbuf~z4CGZz9n)#tN%R@ZQ4xvC4!^Rg#$TdKowQ?Q^ zI>-b`^%Y>~JVTv`snr+YKVp-V5Z*C3b$9N&f@+T#%Iw~9FEnTJckqXL+3W>Rp4qK- zi08nDg1r%fCfG0RhBQX|(9CpxR@gtzb|_|Yuke4%KjE$jt66SMZ@HM)1PewUY-3aX zq0L!GThcNB0{gr{s&Z>tviJck1`EKvqYOkHNG8GWa2AiCb~2~S5=jP4k)K5-2G=de z@hOqVhL!q;NiGxOLsSP%ov1Sb=+}oj#t}pbG5sw=(imBPY|-4;r@Sfv#>a@{=^LRV zObm)EA<~iz6!c{b(gxx%6hE0{MD%8&>f|j_GIWhQCpe1`v!@V9MLcr+Wj)+oH=GTp zaG>ugwz}faG_uE_^@#%QL-#TQAe%toRqk2{sZB)JsuAi+;_r-RfU!WyOd#_xGLTt^ z$8{b8LK2YOd7A`}KT^>dn$_p<)Ccl$L3PS5^6@uw;jek_3$_Y^Pd)PD5qjdtgl9Ww z2z@<%7WtIFG&9hz5p*+>*~bLlEd}d^$5BxYHf5#e0tpro_Oki;Xe_PPHJbfVWhjTn zoKyFw$MyHw)H7`pP}*1M6zzn*4#AraTfu@Fk38d)o&p~Ox@BVABMW3xt0=ii+Udc> zqy~<;gVpHrDB>x!r{v|e1No!H(V~dN50k?X^r;~W74;iN3L{J;6pwQ)!bk@j`xpWB zSk;_dRoU~ECm$_=GB2)FA0+!k{GKB>2Pn-dfq(^d`=cq+4|67p0c0&{Ees|so`HhE z-K3MlIbsiByr~>H8;&@v^+9F3?1rgtb=n3!ske(aSCC4a@`TTR^a#XGpd6zUhxh}4 zN?`@Pkph+NbVTA&kqrofEz<-gOCw$NJ){c5Jr})uGbD=wWy$CGr66SLgdSX2^O~IB z5W~kiBCF)*tg$Nl*8e!aVE{J<(A4eeaFm>o1E<{{SGp79jGaV7Bfqpc0X7W8XCtmb zykGbi$c&Cq#U0x>e3L9PAz8$qh?}P)-go8_%B^-pcNL1UJ@z1gH!M34kKSvqwtxv_ zY2fbMyu5!A7y{b<5t*gMQqsQ-hdzJLy#*=gm4hw&?uOiPxe<>-6}T4g`LT<8*2`s% zbmh=XCs?IAhD=5Npj@%ro0o3>1a)tIe?6ccYhS6?NYpmipe(z_^@EozGnRN-_7;?u z7b>55x$h3s!C1{r7TPtBtSKusADIYML)InsG=b7$zv1dQ6@nQ_$6M)|wwAS*EP&9S*j#G8l1$>3zw8NIp z)c5qsc2w&I&;6p657Un8Wne>Z`{xdykd9Au+ z6@>PlkjxkgJ6L!6#yI$ovm=wS9B+8OTcu(Wp~@f&O7dN$ac$$wB9lB8`m+qrb<3YD z7>PP3>6XA`9h72Kg9cI{!r6UMSuy%dXo|O0Tw`t`Zv%)>X_fNale)8#6jLHY3lMZ` zi4da^;bd(I5yz|Z{1F{QW}HU`D9?c9o2I#YZ{yUsSRo+Ae%k``mcS$AmkI8hth5gXqE?<&@0PEVI9T++%=Jj(cfF<7OvXr7{$ z@5B&=UQRb>31hx&2|G>4nzl9`Lzi=RRl>$wWwvLODc|1y)N4{i2`e_h9szyh>Hf%9$Rt|`@&riR$!6;5^==}v zZwbKT(m~buyZB5#y!%ku7xf;se))1idGeh6JM94D=<(u@T|Rr%)b~Mh>WE-(-e?YB z|B}5C?SDJ^^81GDWqMP915oy>`t6_{6zP*>&QqGF{Qy*Q7AAs@j3%X27f+nj5X@0S z+~@Ec4MEhThgU9Trmfbun4cx$8Vsgb4um*+G?4|cBtn+SC>VlB-`~@O2XP{O4V0Bc z*)$rCu6-4oz+4PM?Xi-$VaWndEX7X>q^v9OaLu~DA8QUcc4&GKu8~g=r`-8wTr54C zgsJ=OX?IW>SV`Af^h(SzozYYlPSf$df>4Tj*LN57Ewsy#e-bd6${mmD_C$3h@|L>t z;-Que$G|d(ge?kM7e^C`(|&AvY&9TZd+xe01<%!`W8IzQAg*Ikm2TG5A2Y&B#iq5| z#;l25$GDj)M?vYVgqF=)M6xA2gAp+sIXLfMU`_y(Du-QZ_QI(|E03(C81@;ojxv>9h|b%)KkqW%DBtq2uDnR z{*t^a)-58>0gbJN`v^!y)QSzUhz++X9tn39(umKu`PVI-vW)p4p`Dqn%o$~LAbehX zfBSXx)%T5jiQDb&&<)?`cXv*L6C(>3>Os&KYWi@JCq3!}!55vI*AJ1~y}1)MUo{#= zChul2BT}>;tc8V}Dx&@KPWht#36{yXJLm-RkDi!qCf*c<$>S*753<7?$4bz^_8qjD zYW4H}d*>S_%%1+;7hlxgySdNp_0S$(_xk60`TK7C%J$9W+0P?QYe??_%M-imhY#@e z8EuawAiKYw6Qif6rRA*;1`45O8WVk)<7dCw&tq-plPnBA=TsNmjkvsXLL}JEO31!- zbtXTH_|$HK*T`w8H%XN3D=*^1!YL@mZQ=B}Xy;||LbSte;SzcFV@qyY3x0Q7Vp(pZ ztv#<4-)%HU`TKVKU{=*-!(2dc`hU;3;Hu367tjF!JazuR zGcH?pTN)F0d&l4WAqzWOr~lva)~_DGp2pDQw?^Z4{m+QYk%onljp2WI|7q(trPQT~ePtV80Nu=+`FKU|DOt32yT}xF9$4!LA?xlwe4+=v8SO#VjsCOk& zOI19zVRsmnjku-Im6@MUvO63)0y(Q06c50MN(34PRC*dJ>z2JM=}JL##ob^FW{4a- z38WjDheQ=?C@c;q{_P&U#1~}c3ExBBwRHwM+Iq_HY=PZTX8>R6uV0NPP*8HG21*xT zJK$u{m4?dgB8LYli}oy;307_M$C@#5BR6ezTleG_OGUdtfE3O2O!9)*sW3=`cfbU4ii<6ud;f$;@mh57`+q z>7*MyUO!Q&WwO~rkGSM~H&lvgg^V$$LXtCAE70P(E!#XCIVh1C6PL`iS0s!)eTXang%skiQp|*-6cYI4GDsUShxT z&4@8Z(E?P81J%YjjbK>ftmxL7_KqVHSYvM>AoIxTjs=w==Sjqe*u40zBPOteu~!LF zg#K28MHBUfIZJM-24g5O+BOcy04GP!kQ})H<57|p4;pdKU4t`H8frLWtI|ydWgAxp zKbfe4h}q0=cJ6*~hu!2-m)&x`9?;z@p-+nT{Q{`|8lO7L^`(3$x_R+rjm#IEfQ&|- zhO6?En7&DpHAl^Iy$0Tft4sA-{Tvhm6cq1545!HVV)_|p1G%(;W)u2kwyC8S@YoQF zk{*)jtThdzIw23^Q4Tr`+`^`-D4K{G7aBFNUBX}~3$H+Z`|L#-*KW7kn|Lk@>`?dY zhsh!lLSKK5m?1NS29C1D@Sq7?TSaq@7B>Ak^Zl^fA&GCWq+7=b&DOj@i`_V-D-5Yi z2_74_-WOM&&txB52gFygUtxJ~su>4Ab$D$#3YY^IC!n25Z>J43>`$3zcV22SXruRs z*6DGu?XYB)7MoN2&_%J?*v7|Sgklz?S0}n(HK8zEw_jLUz8!n+5(wxhIYc+{UAK3*bJpon z7O+c==8`U8n-VBuyDd0xLUGd@3z>tLpep-<+N?RL3TBU%5ID2aJ8;4~sdwnioBVdT z)jyjMYr%Z{ko7xcvqLognnOTVO4y1Xgm*DVHVI&Gbqs!80a{g%cDz)Pg+$!E$RBis zP~2>M?q4VGe89bw3724n+O$io?42%1}n_&sb1`X#oIT7q?tLEExm znusEIRF{e}V&U$m=KvW%Q+O)KA6siCgH}cA4V>aZO>9~+tFo%Gth$5*{-DM~as>qy z)8(dwEQqHMlQ*(8@~!AOMdb97(74C@FjdB{;d}ff@H4qGwDFI>b3aL?jh6)P&t8+rw% zU#F!id>wq{ot|_Vv}+XLToPX@3&9I(%Q7tH@U^%#~d58LBOkW!^sMB}u> zAZqUv^fad;~ZmK1ZW#&mf!G=X^Qc@pBZnYQ*HQ^7(D_g^zI;xYj<(1*%;| zV=~e99o*kndfe{iiB{Efzv_K6PruK{_S{bXw%@1wMwq zMt=Zii}w2S_^GAe+ZX$WjT5OxJ^_0SPpjWse-hxW1Z4Z?K!Mq}!`q`3kr=eHxcu#k zK_FV8{b#Km2bAA5TMjJ#xY&!A;jp zTm0ctK)O<%7oZH|Xc>y?xzqMiBjs5#f+;c#K?`EZj3whG z{DN#uDP&*DJs8PEOml{Iw$P}~I^4-;Nm+w!(WuCpK`7prEpAv$*n4AsAF>$>q#@w* zz>AR#E^)?OJ1-=l3zt_(StSYIcr&&TWXe%zqTPhi|m+*r5F#lEQ`%(4Xeh?6>af~j5`&~vEGFxQwgxIZiAn6*VF-87nD zRpu7lU8Jp>MLo%WS@@oMGZg+!hTIyPF&d%8s|nIlbsd9~#!F+a;wz)xzQ zy^uGtScVAAH6(Eg*AdDDnX@pYaa5_c{OiYB!Rs~yEH6N4^?!;2u+5gtc#A4V895;y zrU2RC7p69zcY}Q##bZsT$;9{It+j7I0@ZGk=6UaKBnV~MvwV!zhjTwPzuB_{o=}zQ z+xnoYdZ8=f2-+JRfRQ?Oo$pt7@(}e`P(4 z$l>bip+uG%Rr=JaI@6j3cLvaukaIjf427HpjF0!eV(}z@P2xlZ5DUCc1f3v>(6*{$ zuwT0D*KPK9jEEflF-Un@0vHXi-;)m{Xj?d^-$PDM2zRA&qCKdnSyoaB`IrC3!#F7S zq(0VsRN5x!_FFlR2{V>UCK!;(isr-vYk?rhQ)nh}mCbgvZ|;Xd2i6MP{jz?nESMe2 z=dRoIq@9#Edw=yYSaf(;1bZn(I~cylYQWp_de+f1cWBQKZ-{Qw(nO0F8z_sQxhECh zq6vq7UsEi_Hl8#A7=3zeKl$;n(W_GjMugXskykYW4wzak&F0G13az=Dm>ZO!iH+rU zwVUdc( zI)AevgIjp*+V`$&7SobQpOEZ`xJqq~+?Hhj@+snxp_EmF@Iq`O$XTouPdo7R9^2RtQ>x-TjusdD6C3Y=F4WIx4}341&7_dR*~nOm$`HL@gS|U3KP)%RkLrg$ zG$M4}HWh9RiN;-Hz!HL6g+3`BO)_5oC>qBEPTY1tHB}Y4C_t&iS#Q;ydXnA^WJj?} z*W3-@@4Y$+9*KdY)fP|I$Kl44T7XHZm`RIhko?NrapI`W3VOc2z1b-6RKJ`e>y*yv z;leFU$7_1=uz&I5J!?iD?^-D=EOz{XPo$TsKhChEB3Wik8KPye^J?1dRz22_X0Of# zg~jsvKuTMyo=KhaXs8m_j7J;blR<~yE!;CAB`SI&f&k=EXBdo4hj_2Jr?fm$Za-lz zF#%32Ez;VHq!>qshDzTssg{4|vT%91J^psR&hH{X9*9)3R?Wq-o#qeQ{lc7WNhwq} zpn}T~U~NuSKCW?xXwn>pSe|d|8Nk(-ar~WjJpM8&$D>u+`mfSoV~N&0p-F^y{22R17`1z02}p1)0*;$G(Gj;!)Wn-~utgs`QWbY2iDcKeS6Jyh zSeSae15ik*LPdm=zLtn=Zw)8S(UECwuKkFLaS11P6f#@}d!*DNW%j3qk?aCo2MCiTsOT`}Sov!@wk!WXClnJX@as8DwmO zB~Rkup5?jDC-@3)hQ9R}HJ{_ojpSy}{An;x^tLV(S=&0#1u_`{Xcb2dsim3!ELOhW z=BQYHMQn(R(~L#C=pip;V#uqW%+qdxygF^KV+^%eLVijbs%11xhpN4TvedkuYd5)# z!+`Xkzmb4o_J@g}WH@9<3ZNHOYibgx6+!@YJbi>oj4

9n%xWi~(r{j)rXk^rBr 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

%*gUHnn^|0xP`gicU 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 WdQ(|B?4P>8pO3(^4u6Nliu^y0*WNt< diff --git a/packages/@n8n/workflow-sdk/package.json b/packages/@n8n/workflow-sdk/package.json index aee42010cf4..f015d835426 100644 --- a/packages/@n8n/workflow-sdk/package.json +++ b/packages/@n8n/workflow-sdk/package.json @@ -37,10 +37,6 @@ "./prompts/node-selection": { "types": "./dist/prompts/node-selection/index.d.ts", "default": "./dist/prompts/node-selection/index.js" - }, - "./examples-loader": { - "types": "./dist/examples-loader.d.ts", - "default": "./dist/examples-loader.js" } }, "scripts": { @@ -60,13 +56,8 @@ "generate-types": "npx tsx src/generate-types/generate-types.ts", "fetch-workflows": "npx tsx scripts/fetch-test-workflows.ts", "create-workflows-zip": "npx tsx scripts/create-workflows-zip.ts", - "create-examples-zip": "npx tsx scripts/create-examples-zip.ts", "json-to-code": "npx tsx src/cli/index.ts json-to-code", - "code-to-json": "npx tsx src/cli/index.ts code-to-json", - "fetch-templates": "npx tsx scripts/fetch-templates.ts", - "regenerate-examples": "npx tsx scripts/regenerate-examples.ts", - "criteria:calibrate": "npx tsx scripts/calibration.ts", - "criteria:coverage": "npx tsx scripts/coverage.ts" + "code-to-json": "npx tsx src/cli/index.ts code-to-json" }, "main": "dist/index.js", "module": "src/index.ts", @@ -87,16 +78,11 @@ ], "prompts/node-selection": [ "./dist/prompts/node-selection/index.d.ts" - ], - "examples-loader": [ - "./dist/examples-loader.d.ts" ] } }, "files": [ - "dist/**/*", - "examples/manifest.json", - "examples/templates.zip" + "dist/**/*" ], "devDependencies": { "@n8n/eslint-plugin-community-nodes": "workspace:*", diff --git a/packages/@n8n/workflow-sdk/scripts/calibration.ts b/packages/@n8n/workflow-sdk/scripts/calibration.ts deleted file mode 100644 index 715b4947b65..00000000000 --- a/packages/@n8n/workflow-sdk/scripts/calibration.ts +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Calibration runner. - * - * Loads `examples/_calibration.json` (hand-tagged expert verdicts), runs the - * rubric over each entry, and reports: - * - Spearman rank correlation between rubric scores and expert verdicts - * - Top disagreements with explanations - * - * Usage: pnpm criteria:calibrate - * - * The calibration file is a manually maintained JSON. Tagging convention: - * { id: 1954, verdict: 'helpful' | 'borderline' | 'not-helpful', rationale: '...' } - * One entry per workflow id. 20-30 entries is the right size to anchor the - * rubric without overfitting. - */ -import * as fs from 'fs'; -import * as path from 'path'; - -import { scoreDetailedTemplate, type ScoreResult } from './criteria'; -import { fetchDetail, loadCachedCatalog, type CatalogEntry } from './fetch-templates'; - -const CALIBRATION_PATH = path.resolve(__dirname, '../examples/_calibration.json'); - -const VERDICT_TO_RANK: Record = { - helpful: 2, - borderline: 1, - 'not-helpful': 0, -}; - -interface CalibrationEntry { - id: number; - // 'TBD' is a placeholder for unlabelled entries; scoring skips them. - verdict: 'helpful' | 'borderline' | 'not-helpful' | 'TBD' | string; - rationale?: string; -} - -interface CalibrationFile { - expert_tagged: CalibrationEntry[]; -} - -interface ScoredCalibrationEntry extends CalibrationEntry { - score: number; - breakdown: ScoreResult['breakdown']; - name: string; -} - -function loadCalibration(): CalibrationFile { - if (!fs.existsSync(CALIBRATION_PATH)) { - throw new Error( - `Calibration file not found at ${CALIBRATION_PATH}. ` + - 'Create it with shape { "expert_tagged": [{"id": N, "verdict": "helpful", "rationale": "..."}] }', - ); - } - return JSON.parse(fs.readFileSync(CALIBRATION_PATH, 'utf-8')) as CalibrationFile; -} - -/** Spearman rank correlation between two parallel arrays. Higher = stronger agreement. */ -export function spearmanCorrelation(xs: number[], ys: number[]): number { - if (xs.length !== ys.length) throw new Error('arrays must have equal length'); - if (xs.length < 2) return 0; - const xRanks = ranksWithTies(xs); - const yRanks = ranksWithTies(ys); - const n = xs.length; - const meanX = xRanks.reduce((a, b) => a + b, 0) / n; - const meanY = yRanks.reduce((a, b) => a + b, 0) / n; - let num = 0; - let denX = 0; - let denY = 0; - for (let i = 0; i < n; i++) { - const dx = xRanks[i] - meanX; - const dy = yRanks[i] - meanY; - num += dx * dy; - denX += dx * dx; - denY += dy * dy; - } - if (denX === 0 || denY === 0) return 0; - return num / Math.sqrt(denX * denY); -} - -/** Average rank for ties (standard for Spearman). */ -function ranksWithTies(values: number[]): number[] { - const indexed = values.map((v, i) => ({ v, i })); - indexed.sort((a, b) => a.v - b.v); - const ranks = new Array(values.length); - let i = 0; - while (i < indexed.length) { - let j = i; - while (j + 1 < indexed.length && indexed[j + 1].v === indexed[i].v) j++; - const avgRank = (i + j) / 2 + 1; - for (let k = i; k <= j; k++) ranks[indexed[k].i] = avgRank; - i = j + 1; - } - return ranks; -} - -async function scoreCalibration( - calib: CalibrationFile, - catalog: CatalogEntry[], -): Promise { - const byId = new Map(catalog.map((c) => [c.id, c])); - const out: ScoredCalibrationEntry[] = []; - for (const entry of calib.expert_tagged) { - if (!Object.hasOwn(VERDICT_TO_RANK, entry.verdict)) { - console.warn(` skipping ${entry.id}: verdict not yet labelled (got "${entry.verdict}")`); - continue; - } - const cat = byId.get(entry.id); - if (!cat) { - console.warn(` skipping ${entry.id}: not in cached catalog`); - continue; - } - const detail = await fetchDetail(entry.id); - if (!detail) { - console.warn(` skipping ${entry.id}: detail fetch failed`); - continue; - } - const result = scoreDetailedTemplate(cat, detail, []); - out.push({ - ...entry, - score: result.total, - breakdown: result.breakdown, - name: detail.data.attributes.name, - }); - } - return out; -} - -function printTopDisagreements(scored: ScoredCalibrationEntry[]): void { - const expertRanks = ranksWithTies(scored.map((s) => VERDICT_TO_RANK[s.verdict])); - const rubricRanks = ranksWithTies(scored.map((s) => s.score)); - const disagreements = scored.map((s, i) => ({ - entry: s, - expertRank: expertRanks[i], - rubricRank: rubricRanks[i], - gap: Math.abs(expertRanks[i] - rubricRanks[i]), - })); - disagreements.sort((a, b) => b.gap - a.gap); - console.log('\nTop disagreements (rubric rank vs expert rank):'); - for (const d of disagreements.slice(0, 10)) { - const direction = d.rubricRank > d.expertRank ? 'overrated' : 'underrated'; - console.log( - ` ${direction.padEnd(10)} gap=${d.gap.toFixed(1).padStart(4)} | ` + - `rubric=${d.rubricRank.toFixed(0).padStart(3)} expert=${d.expertRank.toFixed(0).padStart(3)} | ` + - `verdict=${d.entry.verdict.padEnd(11)} | ${d.entry.id} ${d.entry.name.slice(0, 50)}`, - ); - if (d.entry.rationale) console.log(` expert: "${d.entry.rationale}"`); - } -} - -async function main() { - const calib = loadCalibration(); - console.log(`Loaded ${calib.expert_tagged.length} calibration entries\n`); - - const catalog = loadCachedCatalog(); - const scored = await scoreCalibration(calib, catalog); - - if (scored.length < 2) { - console.error('Not enough scored entries to compute correlation'); - process.exit(1); - } - - const expertScores = scored.map((s) => VERDICT_TO_RANK[s.verdict]); - const rubricScores = scored.map((s) => s.score); - const rho = spearmanCorrelation(expertScores, rubricScores); - - console.log(`Calibration set size: ${scored.length}`); - console.log(`Spearman rank correlation: ${rho.toFixed(3)}`); - console.log(`Threshold (Phase 1 ships at): 0.700`); - console.log(`Status: ${rho >= 0.7 ? 'PASS' : 'TUNE WEIGHTS'}`); - - printTopDisagreements(scored); -} - -if (require.main === module) { - main().catch((error) => { - console.error(error); - process.exit(1); - }); -} diff --git a/packages/@n8n/workflow-sdk/scripts/coverage.test.ts b/packages/@n8n/workflow-sdk/scripts/coverage.test.ts deleted file mode 100644 index c2a9c7bf4d0..00000000000 --- a/packages/@n8n/workflow-sdk/scripts/coverage.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { extractKeywords, matchPrompt } from './coverage'; - -describe('coverage helpers', () => { - describe('extractKeywords', () => { - it('lowercases, splits on whitespace, drops stopwords and short tokens', () => { - const out = extractKeywords('Create a workflow that posts to Slack'); - expect(out).toContain('posts'); - expect(out).toContain('slack'); - expect(out).not.toContain('the'); - expect(out).not.toContain('a'); - expect(out).not.toContain('to'); - }); - - it('mixes in tag tokens', () => { - const out = extractKeywords('handle webhook submissions', ['google-sheets', 'multi-action']); - expect(out).toContain('webhook'); - expect(out).toContain('google'); - expect(out).toContain('sheets'); - expect(out).toContain('multi'); - expect(out).toContain('action'); - }); - - it('dedupes repeated tokens', () => { - const out = extractKeywords('slack slack slack notify slack'); - expect(out.filter((t) => t === 'slack').length).toBe(1); - }); - - it('removes punctuation', () => { - const out = extractKeywords('Notify the team in #general about it!'); - expect(out).toContain('notify'); - expect(out).toContain('team'); - expect(out).toContain('general'); - }); - }); - - describe('matchPrompt', () => { - const wf = { - id: 1, - slug: 'gmail-to-slack', - name: 'Gmail to Slack notifier', - description: 'Forwards new Gmail messages to a Slack channel', - nodes: ['n8n-nodes-base.gmailTrigger', 'n8n-nodes-base.slack'], - tags: ['trigger:gmail', 'integration:slack'], - triggerType: 'gmail', - hasAI: false, - }; - - it('counts substring matches across name + description + nodes + tags', () => { - const result = matchPrompt(['gmail', 'slack', 'notify'], wf); - expect(result.matches).toBe(2); // gmail, slack - expect(result.matchedKeywords).toEqual(expect.arrayContaining(['gmail', 'slack'])); - }); - - it('returns 0 matches when no keywords overlap', () => { - const result = matchPrompt(['airtable', 'discord'], wf); - expect(result.matches).toBe(0); - }); - - it('matches case-insensitively', () => { - const result = matchPrompt(['GMAIL', 'SLACK'], wf); - expect(result.matches).toBe(2); - }); - }); -}); diff --git a/packages/@n8n/workflow-sdk/scripts/coverage.ts b/packages/@n8n/workflow-sdk/scripts/coverage.ts deleted file mode 100644 index 9dd403d88d3..00000000000 --- a/packages/@n8n/workflow-sdk/scripts/coverage.ts +++ /dev/null @@ -1,324 +0,0 @@ -/** - * Coverage check: do our curated templates cover the patterns real users ask for? - * - * Uses the existing instance-ai eval prompt corpus as a held-out test set. - * For each eval prompt, we extract a small keyword bag (filtered tokens from - * the prompt + the prompt's structural tags) and look for any manifest entry - * whose name+description+nodes+tags blob matches ≥2 keywords. Coverage = % - * of prompts with at least one match. - * - * This is intentionally a smoke test — keyword overlap will miss semantic - * matches and over-credit name overlap. The real signal lives in Phase 3 - * telemetry on `Builder template read`. Coverage just stops us shipping a - * curated set with obvious gaps. - * - * Usage: pnpm criteria:coverage - */ -import * as fs from 'fs'; -import * as path from 'path'; - -const MANIFEST_PATH = path.resolve(__dirname, '../examples/manifest.json'); -const EVAL_DIR = path.resolve(__dirname, '../../instance-ai/evaluations/data/workflows'); -const REPORT_PATH = path.resolve(__dirname, '../examples/_coverage-report.json'); - -const COVERAGE_TARGET = 0.7; -const MIN_KEYWORD_MATCHES = 3; - -/** - * Words too generic to be useful matches. We want integration names, action - * verbs specific to a domain, and trigger types — not "send", "http", "node", - * "data", which appear in nearly every workflow. - */ -const STOPWORDS = new Set([ - // Articles, conjunctions, prepositions - 'a', - 'an', - 'and', - 'are', - 'as', - 'at', - 'be', - 'but', - 'by', - 'for', - 'from', - 'has', - 'have', - 'i', - 'if', - 'in', - 'is', - 'it', - 'its', - 'me', - 'my', - 'of', - 'on', - 'or', - 'so', - 'the', - 'their', - 'them', - 'this', - 'that', - 'to', - 'was', - 'we', - 'were', - 'will', - 'with', - 'into', - 'over', - 'under', - 'they', - 'one', - 'two', - 'three', - 'been', - 'being', - // Generic shape verbs - 'create', - 'use', - 'used', - 'using', - 'should', - 'can', - 'configure', - 'set', - 'setup', - 'send', - 'sent', - 'sends', - 'sending', - 'fetch', - 'fetches', - 'fetching', - 'do', - 'done', - 'add', - 'adds', - 'adding', - 'get', - 'got', - 'gets', - 'getting', - 'make', - 'makes', - // Workflow-shape generic - 'workflow', - 'workflows', - 'node', - 'nodes', - 'data', - 'request', - 'response', - 'message', - 'messages', - 'item', - 'items', - 'value', - 'values', - 'field', - 'fields', - 'result', - 'results', - 'name', - 'names', - 'http', - 'https', - 'api', - 'url', - 'auth', - 'options', - 'parameters', - 'credentials', - 'credential', - 'output', - 'input', - 'call', - 'calls', - 'com', - 'org', - 'net', - // Submission/contact tokens (too generic) - 'submit', - 'submitted', - 'submission', - 'submissions', - 'submits', - 'complete', - 'completely', - 'possible', - 'help', - 'please', - 'thanks', - 'don', - 'all', - 'every', - 'each', - 'when', - 'then', - 'how', - 'what', - 'about', - 'via', - 'out', - 'ask', - // Single chars from contractions - 't', - 'm', - 's', - 're', - 've', - 'll', - 'd', -]); - -interface ManifestWorkflow { - id: number; - slug: string; - name: string; - description: string; - nodes: string[]; - tags: string[]; - triggerType: string; - hasAI: boolean; -} - -interface ManifestFile { - workflows: ManifestWorkflow[]; -} - -interface EvalPrompt { - prompt: string; - complexity?: string; - tags?: string[]; - triggerType?: string; -} - -interface PromptCoverageResult { - prompt_file: string; - prompt: string; - keywords: string[]; - matched: boolean; - top_match: { slug: string; matches: number; matched_keywords: string[] } | null; -} - -export function extractKeywords(text: string, extraTags: string[] = []): string[] { - const tokens = text - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, ' ') - .split(/\s+/) - .filter((t) => t.length >= 3 && !STOPWORDS.has(t)); - const tagTokens = extraTags - .flatMap((t) => t.toLowerCase().split(/[-\s]+/)) - .filter((t) => t.length >= 3 && !STOPWORDS.has(t)); - const dedup = new Set([...tokens, ...tagTokens]); - return Array.from(dedup); -} - -export function matchPrompt( - keywords: string[], - workflow: ManifestWorkflow, -): { matches: number; matchedKeywords: string[] } { - const haystack = [ - workflow.name, - workflow.description, - workflow.nodes.join(' '), - workflow.tags.join(' '), - workflow.triggerType, - ] - .join(' ') - .toLowerCase(); - - const matched: string[] = []; - for (const k of keywords) { - if (haystack.includes(k.toLowerCase())) matched.push(k); - } - return { matches: matched.length, matchedKeywords: matched }; -} - -function loadEvalPrompts(): Array<{ filename: string; data: EvalPrompt }> { - if (!fs.existsSync(EVAL_DIR)) { - throw new Error(`Eval directory not found at ${EVAL_DIR}`); - } - const files = fs.readdirSync(EVAL_DIR).filter((f) => f.endsWith('.json')); - return files.map((f) => ({ - filename: f, - data: JSON.parse(fs.readFileSync(path.join(EVAL_DIR, f), 'utf-8')) as EvalPrompt, - })); -} - -function loadManifest(): ManifestFile { - if (!fs.existsSync(MANIFEST_PATH)) { - throw new Error( - `Manifest not found at ${MANIFEST_PATH}. Run \`pnpm regenerate-examples\` first.`, - ); - } - return JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf-8')) as ManifestFile; -} - -function main() { - const manifest = loadManifest(); - const prompts = loadEvalPrompts(); - - const results: PromptCoverageResult[] = []; - for (const { filename, data } of prompts) { - const keywords = extractKeywords(data.prompt, data.tags ?? []); - let bestMatch: { slug: string; matches: number; matched_keywords: string[] } | null = null; - for (const wf of manifest.workflows) { - const m = matchPrompt(keywords, wf); - if (m.matches >= MIN_KEYWORD_MATCHES) { - if (!bestMatch || m.matches > bestMatch.matches) { - bestMatch = { slug: wf.slug, matches: m.matches, matched_keywords: m.matchedKeywords }; - } - } - } - results.push({ - prompt_file: filename, - prompt: data.prompt.slice(0, 200), - keywords, - matched: bestMatch !== null, - top_match: bestMatch, - }); - } - - const covered = results.filter((r) => r.matched).length; - const ratio = covered / results.length; - - const report = { - generatedAt: new Date().toISOString(), - total_prompts: results.length, - covered, - uncovered: results.length - covered, - coverage: Number(ratio.toFixed(3)), - target: COVERAGE_TARGET, - passed: ratio >= COVERAGE_TARGET, - results, - }; - - fs.writeFileSync(REPORT_PATH, JSON.stringify(report, null, 2)); - - console.log( - `Coverage: ${covered}/${results.length} (${(ratio * 100).toFixed(0)}%) — target ${(COVERAGE_TARGET * 100).toFixed(0)}%`, - ); - console.log(`Status: ${ratio >= COVERAGE_TARGET ? 'PASS' : 'TUNE'}`); - console.log(`Wrote ${path.relative(process.cwd(), REPORT_PATH)}\n`); - - const uncovered = results.filter((r) => !r.matched); - if (uncovered.length > 0) { - console.log('Uncovered prompts:'); - for (const r of uncovered) { - console.log(` - ${r.prompt_file}: "${r.prompt.slice(0, 80)}..."`); - } - } -} - -if (require.main === module) { - try { - main(); - } catch (error) { - console.error(error); - process.exit(1); - } -} diff --git a/packages/@n8n/workflow-sdk/scripts/create-examples-zip.ts b/packages/@n8n/workflow-sdk/scripts/create-examples-zip.ts deleted file mode 100644 index 85466d2316e..00000000000 --- a/packages/@n8n/workflow-sdk/scripts/create-examples-zip.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Packages all workflow JSON files from `examples/workflows/` into a single - * `examples/templates.zip` for committing. The manifest stays as a separate - * committed file — it is the source of truth and not zipped. - * - * Usage: pnpm create-examples-zip - */ -import AdmZip from 'adm-zip'; -import * as fs from 'fs'; -import * as path from 'path'; - -const EXAMPLES_DIR = path.resolve(__dirname, '..', 'examples'); -const WORKFLOWS_DIR = path.join(EXAMPLES_DIR, 'workflows'); -const ZIP_PATH = path.join(EXAMPLES_DIR, 'templates.zip'); - -function createExamplesZip(): void { - if (!fs.existsSync(WORKFLOWS_DIR)) { - console.error(`Error: workflows dir not found at ${WORKFLOWS_DIR}`); - console.error('Run `pnpm regenerate-examples` first.'); - process.exit(1); - } - - const zip = new AdmZip(); - const files = fs.readdirSync(WORKFLOWS_DIR).filter((f) => f.endsWith('.json')); - - if (files.length === 0) { - console.error(`Error: no workflow JSON files found in ${WORKFLOWS_DIR}`); - process.exit(1); - } - - for (const file of files) { - zip.addLocalFile(path.join(WORKFLOWS_DIR, file)); - } - - zip.writeZip(ZIP_PATH); - console.log(`Created: ${ZIP_PATH}`); - console.log(`Contents: ${files.length} workflows`); -} - -createExamplesZip(); diff --git a/packages/@n8n/workflow-sdk/scripts/criteria.test.ts b/packages/@n8n/workflow-sdk/scripts/criteria.test.ts deleted file mode 100644 index 99854632fd7..00000000000 --- a/packages/@n8n/workflow-sdk/scripts/criteria.test.ts +++ /dev/null @@ -1,452 +0,0 @@ -import { - mechanicalGateCatalog, - mechanicalGateDetail, - tractionScore, - recencyScore, - coverageScore, - clarityScore, - densityScore, - bucketKey, - bucketKeyToString, - scoreCatalogEntry, - scoreDetailedTemplate, - hasAI, - WEIGHTS, - NODE_COUNT_MIN, - NODE_COUNT_MAX, - __test, - type BucketKey, -} from './criteria'; -import type { CatalogEntry, DetailResponse } from './fetch-templates'; - -// --------------------------------------------------------------------------- -// Fixture builders -// --------------------------------------------------------------------------- - -function makeCatalog(overrides: Partial = {}): CatalogEntry { - return { - id: 1, - name: 'Test Workflow', - totalViews: 1000, - createdAt: new Date().toISOString(), - user: { username: 'someone', verified: true }, - purchaseUrl: null, - nodes: Array.from({ length: 5 }, (_, i) => ({ name: `node-${i}` })), - ...overrides, - }; -} - -function makeDetail( - opts: { - nodes?: Array>; - connections?: Record; - views?: number; - recentViews?: number; - updatedAt?: string; - status?: string; - } = {}, -): DetailResponse { - return { - data: { - id: 1, - attributes: { - name: 'Detail', - description: '', - workflow: { - nodes: opts.nodes ?? [ - { id: 't', name: 'Schedule Trigger', type: 'n8n-nodes-base.scheduleTrigger' }, - { id: 'a', name: 'My Slack Post', type: 'n8n-nodes-base.slack' }, - { id: 'b', name: 'Format Output', type: 'n8n-nodes-base.set' }, - ], - connections: opts.connections ?? {}, - }, - createdAt: '2024-01-01T00:00:00Z', - updatedAt: opts.updatedAt ?? new Date().toISOString(), - views: opts.views ?? 1000, - recentViews: opts.recentViews ?? 5, - hidden: false, - username: 'someone', - status: opts.status ?? 'published', - price: null, - difficulty: null, - readyToDemo: null, - }, - }, - meta: {}, - }; -} - -// --------------------------------------------------------------------------- -// Mechanical gate -// --------------------------------------------------------------------------- - -describe('mechanicalGateCatalog', () => { - it('passes a typical free verified workflow', () => { - expect(mechanicalGateCatalog(makeCatalog())).toEqual({ ok: true }); - }); - - it('rejects paid via purchaseUrl', () => { - const r = mechanicalGateCatalog(makeCatalog({ purchaseUrl: 'https://example.com/buy' })); - expect(r.ok).toBe(false); - }); - - it('rejects paid via positive price', () => { - const r = mechanicalGateCatalog(makeCatalog({ price: 5 })); - expect(r.ok).toBe(false); - }); - - it('rejects unverified author', () => { - const r = mechanicalGateCatalog(makeCatalog({ user: { username: 'rando', verified: false } })); - expect(r.ok).toBe(false); - }); - - it('rejects oversized workflow at the catalog stage', () => { - const big = makeCatalog({ - nodes: Array.from({ length: NODE_COUNT_MAX + 5 }, (_, i) => ({ name: `n${i}` })), - }); - const r = mechanicalGateCatalog(big); - expect(r.ok).toBe(false); - }); - - it('does NOT enforce lower bound at catalog stage (list nodes are sparse)', () => { - const small = makeCatalog({ nodes: [{ name: 'x' }] }); - expect(mechanicalGateCatalog(small)).toEqual({ ok: true }); - }); -}); - -describe('mechanicalGateDetail', () => { - it('passes a typical workflow with trigger', () => { - expect(mechanicalGateDetail(makeDetail())).toEqual({ ok: true }); - }); - - it('rejects unpublished status', () => { - const r = mechanicalGateDetail(makeDetail({ status: 'draft' })); - expect(r.ok).toBe(false); - }); - - it('rejects too-small node count', () => { - const r = mechanicalGateDetail( - makeDetail({ - nodes: Array.from({ length: NODE_COUNT_MIN - 1 }, (_, i) => ({ - id: String(i), - type: 'n8n-nodes-base.set', - })), - }), - ); - expect(r.ok).toBe(false); - }); - - it('rejects too-large node count', () => { - const r = mechanicalGateDetail( - makeDetail({ - nodes: Array.from({ length: NODE_COUNT_MAX + 1 }, (_, i) => ({ - id: String(i), - type: 'n8n-nodes-base.set', - })), - }), - ); - expect(r.ok).toBe(false); - }); - - it('rejects when no trigger present', () => { - const r = mechanicalGateDetail( - makeDetail({ - nodes: [ - { id: '1', type: 'n8n-nodes-base.set' }, - { id: '2', type: 'n8n-nodes-base.slack' }, - { id: '3', type: 'n8n-nodes-base.gmail' }, - ], - }), - ); - expect(r.ok).toBe(false); - if (!r.ok) expect(r.reason).toContain('trigger'); - }); -}); - -// --------------------------------------------------------------------------- -// Per-dimension scorers -// --------------------------------------------------------------------------- - -describe('tractionScore', () => { - it('is monotonic in views', () => { - expect(tractionScore(10, 0)).toBeLessThan(tractionScore(1000, 0)); - expect(tractionScore(1000, 0)).toBeLessThan(tractionScore(100000, 0)); - }); - - it('is bounded in [0, 1]', () => { - expect(tractionScore(0, 0)).toBe(0); - expect(tractionScore(10_000_000, 10_000_000)).toBeLessThanOrEqual(1); - }); -}); - -describe('recencyScore', () => { - const now = new Date('2026-05-07T00:00:00Z').getTime(); - - it('is 1.0 for fresh workflows (≤90d)', () => { - const updatedAt = new Date(now - 30 * 24 * 3600 * 1000).toISOString(); - expect(recencyScore(updatedAt, now)).toBe(1); - }); - - it('is 0 for workflows older than 2y', () => { - const updatedAt = new Date(now - 1000 * 24 * 3600 * 1000).toISOString(); - expect(recencyScore(updatedAt, now)).toBe(0); - }); - - it('decays linearly between 90d and 2y', () => { - const oneYearAgo = new Date(now - 365 * 24 * 3600 * 1000).toISOString(); - const score = recencyScore(oneYearAgo, now); - expect(score).toBeGreaterThan(0); - expect(score).toBeLessThan(1); - }); -}); - -describe('coverageScore', () => { - const bucket: BucketKey = { - triggerType: 'webhook', - primaryIntegration: 'slack', - hasAI: false, - controlFlowKind: 'linear', - }; - - it('is 1.0 for an empty running set', () => { - expect(coverageScore(bucket, [])).toBe(1); - }); - - it('halves with each duplicate bucket', () => { - expect(coverageScore(bucket, [bucket])).toBeCloseTo(0.5); - expect(coverageScore(bucket, [bucket, bucket])).toBeCloseTo(1 / 3); - }); - - it('is unaffected by other buckets', () => { - const other: BucketKey = { ...bucket, triggerType: 'schedule' }; - expect(coverageScore(bucket, [other, other, other])).toBe(1); - }); -}); - -describe('clarityScore', () => { - it('rewards named nodes, sticky notes, and distinct types', () => { - const detail = makeDetail({ - nodes: [ - { id: 'sticky', type: 'n8n-nodes-base.stickyNote', name: 'Doc' }, - { id: 't', type: 'n8n-nodes-base.scheduleTrigger', name: 'Daily 9am' }, - { id: 'a', type: 'n8n-nodes-base.slack', name: 'Post update' }, - { id: 'b', type: 'n8n-nodes-base.gmail', name: 'Send digest' }, - ], - }); - // All real nodes named, sticky present, ≥3 distinct types → 0.4 + 0.3 + 0.3 = 1.0 - expect(clarityScore(detail)).toBeCloseTo(1.0); - }); - - it('penalises default-named workflows', () => { - const detail = makeDetail({ - nodes: [ - { id: 't', type: 'n8n-nodes-base.scheduleTrigger', name: 'Schedule Trigger' }, - { id: 's1', type: 'n8n-nodes-base.set', name: 'Edit Fields' }, - { id: 's2', type: 'n8n-nodes-base.set', name: 'Edit Fields1' }, - ], - }); - // All default-named, no sticky, only 2 distinct types - expect(clarityScore(detail)).toBeLessThan(0.4); - }); -}); - -describe('densityScore', () => { - it('is 1.0 when every node is a different type', () => { - const detail = makeDetail({ - nodes: [ - { id: 't', type: 'n8n-nodes-base.scheduleTrigger' }, - { id: 'a', type: 'n8n-nodes-base.slack' }, - { id: 'b', type: 'n8n-nodes-base.gmail' }, - ], - }); - expect(densityScore(detail)).toBe(1); - }); - - it('is low when many nodes share types', () => { - const detail = makeDetail({ - nodes: [ - { id: 't', type: 'n8n-nodes-base.scheduleTrigger' }, - { id: '1', type: 'n8n-nodes-base.set' }, - { id: '2', type: 'n8n-nodes-base.set' }, - { id: '3', type: 'n8n-nodes-base.set' }, - ], - }); - // 2 distinct / 4 nodes = 0.5 - expect(densityScore(detail)).toBeCloseTo(0.5); - }); -}); - -// --------------------------------------------------------------------------- -// Bucket key -// --------------------------------------------------------------------------- - -describe('bucketKey', () => { - it('classifies a webhook → slack workflow without AI', () => { - const detail = makeDetail({ - nodes: [ - { id: 't', type: 'n8n-nodes-base.webhook' }, - { id: 'a', type: 'n8n-nodes-base.slack' }, - { id: 'b', type: 'n8n-nodes-base.set' }, - ], - }); - const key = bucketKey(detail); - expect(key.triggerType).toBe('webhook'); - expect(key.primaryIntegration).toBe('slack'); - expect(key.hasAI).toBe(false); - expect(key.controlFlowKind).toBe('linear'); - }); - - it('classifies AI agent workflows', () => { - const detail = makeDetail({ - nodes: [ - { id: 't', type: '@n8n/n8n-nodes-langchain.chatTrigger' }, - { id: 'm', type: '@n8n/n8n-nodes-langchain.lmChatOpenAi' }, - { id: 'a', type: '@n8n/n8n-nodes-langchain.agent' }, - ], - }); - const key = bucketKey(detail); - expect(key.triggerType).toBe('chatTrigger'); - expect(key.hasAI).toBe(true); - }); - - it('detects branching control flow', () => { - const detail = makeDetail({ - nodes: [ - { id: 't', type: 'n8n-nodes-base.scheduleTrigger' }, - { id: 'i', type: 'n8n-nodes-base.if' }, - { id: 'a', type: 'n8n-nodes-base.slack' }, - { id: 'b', type: 'n8n-nodes-base.gmail' }, - ], - connections: {}, - }); - expect(bucketKey(detail).controlFlowKind).toBe('branching'); - }); - - it('detects loop control flow', () => { - const detail = makeDetail({ - nodes: [ - { id: 't', type: 'n8n-nodes-base.scheduleTrigger' }, - { id: 'l', type: 'n8n-nodes-base.splitInBatches' }, - { id: 'a', type: 'n8n-nodes-base.slack' }, - ], - }); - expect(bucketKey(detail).controlFlowKind).toBe('loop'); - }); - - it('detects parallel control flow from connections fan-out', () => { - const detail = makeDetail({ - nodes: [ - { id: 't', type: 'n8n-nodes-base.scheduleTrigger', name: 'Trigger' }, - { id: 'a', type: 'n8n-nodes-base.slack', name: 'A' }, - { id: 'b', type: 'n8n-nodes-base.gmail', name: 'B' }, - ], - connections: { - Trigger: { - main: [ - [ - { node: 'A', type: 'main', index: 0 }, - { node: 'B', type: 'main', index: 0 }, - ], - ], - }, - }, - }); - expect(bucketKey(detail).controlFlowKind).toBe('parallel'); - }); -}); - -describe('bucketKeyToString', () => { - it('produces stable keys', () => { - const a: BucketKey = { - triggerType: 'webhook', - primaryIntegration: 'slack', - hasAI: false, - controlFlowKind: 'linear', - }; - expect(bucketKeyToString(a)).toBe('webhook|slack|noai|linear'); - }); -}); - -// --------------------------------------------------------------------------- -// Composition -// --------------------------------------------------------------------------- - -describe('scoreCatalogEntry', () => { - it('uses traction + recency only (other dims zero)', () => { - const entry = makeCatalog({ totalViews: 10000, createdAt: new Date().toISOString() }); - const result = scoreCatalogEntry(entry); - expect(result.breakdown.coverage).toBe(0); - expect(result.breakdown.clarity).toBe(0); - expect(result.breakdown.density).toBe(0); - expect(result.total).toBeGreaterThan(0); - }); -}); - -describe('scoreDetailedTemplate', () => { - it('combines all weighted dimensions', () => { - const entry = makeCatalog(); - const detail = makeDetail(); - const result = scoreDetailedTemplate(entry, detail, []); - expect(result.breakdown.traction).toBeGreaterThan(0); - expect(result.breakdown.recency).toBeGreaterThan(0); - expect(result.breakdown.coverage).toBe(1); // empty running set - expect(result.total).toBeGreaterThan(0); - }); - - it('drops coverage as duplicates accumulate', () => { - const entry = makeCatalog(); - const detail = makeDetail(); - const bucket = bucketKey(detail); - const empty = scoreDetailedTemplate(entry, detail, []); - const oneDup = scoreDetailedTemplate(entry, detail, [bucket]); - expect(empty.total).toBeGreaterThan(oneDup.total); - }); - - it('aiAgent dimension contributes 0 to total because weight is 0', () => { - const entry = makeCatalog(); - const aiDetail = makeDetail({ - nodes: [ - { id: 't', type: '@n8n/n8n-nodes-langchain.chatTrigger' }, - { id: 'a', type: '@n8n/n8n-nodes-langchain.agent' }, - { id: 'm', type: '@n8n/n8n-nodes-langchain.memoryBufferWindow' }, - { id: 'l', type: '@n8n/n8n-nodes-langchain.lmChatOpenAi' }, - ], - }); - const noAiDetail = makeDetail(); - const aiResult = scoreDetailedTemplate(entry, aiDetail, []); - const noAiResult = scoreDetailedTemplate(entry, noAiDetail, []); - // AI signal is computed but the weight is 0 for now - expect(aiResult.breakdown.aiAgent).toBeGreaterThan(0); - expect(noAiResult.breakdown.aiAgent).toBe(0); - expect(WEIGHTS.aiAgent).toBe(0); - }); -}); - -// --------------------------------------------------------------------------- -// Tagged internals (compact tests, low-risk) -// --------------------------------------------------------------------------- - -describe('internal helpers', () => { - it('vendorOf strips type prefix', () => { - expect(__test.vendorOf('n8n-nodes-base.googleSheets')).toBe('googleSheets'); - expect(__test.vendorOf('@n8n/n8n-nodes-langchain.openAi')).toBe('langchain'); - }); - - it('isDefaultName flags repeated default-style names', () => { - expect(__test.isDefaultName({ name: 'Edit Fields', type: 'n8n-nodes-base.set' })).toBe(true); - expect(__test.isDefaultName({ name: 'Edit Fields1', type: 'n8n-nodes-base.set' })).toBe(true); - expect(__test.isDefaultName({ name: 'My Slack Post', type: 'n8n-nodes-base.slack' })).toBe( - false, - ); - }); - - it('hasAI is true for any langchain-prefixed node', () => { - const detail = makeDetail({ - nodes: [ - { id: 't', type: 'n8n-nodes-base.scheduleTrigger' }, - { id: 'a', type: '@n8n/n8n-nodes-langchain.agent' }, - ], - }); - expect(hasAI(detail)).toBe(true); - }); -}); diff --git a/packages/@n8n/workflow-sdk/scripts/criteria.ts b/packages/@n8n/workflow-sdk/scripts/criteria.ts deleted file mode 100644 index 17972ee28a7..00000000000 --- a/packages/@n8n/workflow-sdk/scripts/criteria.ts +++ /dev/null @@ -1,398 +0,0 @@ -/** - * Helpfulness rubric for template curation. - * - * Tunable knobs are at the top of the file. Documentation lives in - * `docs/template-criteria.md`. The score breakdown is preserved per template - * so manifest entries are reviewable: anyone can see why a template ranked - * where it did. - * - * Two scoring stages: - * - `scoreCatalogEntry()` — runs against cheap list metadata, used to pick - * the top-K candidates worth fetching detail for. - * - `scoreDetailedTemplate()` — runs against full detail JSON, used to - * pick the final manifest set with full diversity bucketing. - */ -import type { CatalogEntry, DetailResponse } from './fetch-templates'; - -// --------------------------------------------------------------------------- -// Tunable weights -// --------------------------------------------------------------------------- - -export const WEIGHTS = { - traction: 20, - recency: 20, - coverage: 35, - aiAgent: 0, // folded into bucket key; lift if telemetry shows under-use - clarity: 15, - density: 5, -} as const; - -export const NODE_COUNT_MIN = 3; -export const NODE_COUNT_MAX = 40; -export const RECENCY_FRESH_DAYS = 90; -export const RECENCY_STALE_DAYS = 730; // 2 years - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export type GateResult = { ok: true } | { ok: false; reason: string }; - -export interface RubricBreakdown { - traction: number; - recency: number; - coverage: number; - aiAgent: number; - clarity: number; - density: number; -} - -export interface ScoreResult { - total: number; - breakdown: RubricBreakdown; -} - -export type TriggerType = - | 'webhook' - | 'schedule' - | 'chatTrigger' - | 'formTrigger' - | 'manual' - | 'telegram' - | 'gmail' - | 'other'; - -export type ControlFlowKind = 'linear' | 'branching' | 'loop' | 'parallel'; - -export interface BucketKey { - triggerType: TriggerType; - primaryIntegration: string; - hasAI: boolean; - controlFlowKind: ControlFlowKind; -} - -// --------------------------------------------------------------------------- -// Mechanical gate -// --------------------------------------------------------------------------- - -/** Catalog-stage gate. List-only metadata. */ -export function mechanicalGateCatalog(entry: CatalogEntry): GateResult { - if (entry.purchaseUrl !== null) return { ok: false, reason: 'paid (purchaseUrl set)' }; - if (entry.price !== null && entry.price !== undefined && entry.price > 0) { - return { ok: false, reason: `paid (price=${entry.price})` }; - } - if (!entry.user?.verified) return { ok: false, reason: 'unverified author' }; - const listNodeCount = entry.nodes?.length ?? 0; - // List nodes is sparse; only enforce upper bound here. Lower bound rechecked at detail. - if (listNodeCount > NODE_COUNT_MAX) { - return { ok: false, reason: `too large (list reports ${listNodeCount} nodes)` }; - } - return { ok: true }; -} - -/** Detail-stage gate. Real workflow JSON. */ -export function mechanicalGateDetail(detail: DetailResponse): GateResult { - const attrs = detail.data.attributes; - if (attrs.status !== 'published') return { ok: false, reason: `status=${attrs.status}` }; - const realNodeCount = attrs.workflow.nodes?.length ?? 0; - if (realNodeCount < NODE_COUNT_MIN || realNodeCount > NODE_COUNT_MAX) { - return { - ok: false, - reason: `nodeCount ${realNodeCount} outside [${NODE_COUNT_MIN}, ${NODE_COUNT_MAX}]`, - }; - } - if (findTriggerNode(detail) === null) { - return { ok: false, reason: 'no trigger node' }; - } - return { ok: true }; -} - -// --------------------------------------------------------------------------- -// Per-dimension scorers -// --------------------------------------------------------------------------- - -/** Traction: log-scaled views; mixes total + recent so freshness matters too. */ -export function tractionScore(totalViews: number, recentViews: number): number { - const total = Math.log10(Math.max(0, totalViews) + 1); - const recent = Math.log10(Math.max(0, recentViews) + 1); - // Normalise to ~[0, 1]: log10(1M) = 6, log10(1K) = 3 → cap divisor at 8 - return Math.min(1, (total + recent) / 8); -} - -/** Recency: 1.0 if updated within N days, 0.0 once stale, linear in between. */ -export function recencyScore(updatedAt: string, now: number = Date.now()): number { - const ageMs = now - new Date(updatedAt).getTime(); - const ageDays = ageMs / (24 * 3600 * 1000); - if (ageDays <= RECENCY_FRESH_DAYS) return 1; - if (ageDays >= RECENCY_STALE_DAYS) return 0; - return 1 - (ageDays - RECENCY_FRESH_DAYS) / (RECENCY_STALE_DAYS - RECENCY_FRESH_DAYS); -} - -/** Marginal coverage: how rare is this template's bucket in the running set? */ -export function coverageScore(bucket: BucketKey, runningSet: BucketKey[]): number { - const key = bucketKeyToString(bucket); - const count = runningSet.reduce( - (n, existing) => (bucketKeyToString(existing) === key ? n + 1 : n), - 0, - ); - return 1 / (1 + count); -} - -/** AI-agent presence (currently weighted 0; signal still computed for telemetry). */ -export function aiAgentScore(detail: DetailResponse): number { - if (!hasAI(detail)) return 0; - if (hasSubnodePattern(detail)) return 1; - return 0.6; -} - -/** Structural clarity: signals of authoring care. */ -export function clarityScore(detail: DetailResponse): number { - const nodes = detail.data.attributes.workflow.nodes ?? []; - let score = 0; - - const realNodes = nodes.filter((n) => !isStickyNote(n)); - const namedRatio = - realNodes.length === 0 - ? 0 - : realNodes.filter((n) => !isDefaultName(n)).length / realNodes.length; - if (namedRatio >= 0.5) score += 0.4; - - const stickyCount = nodes.filter(isStickyNote).length; - if (stickyCount >= 1) score += 0.3; - - const distinctTypes = new Set(realNodes.map((n) => String(n.type ?? ''))).size; - if (distinctTypes >= 3) score += 0.3; - - return score; -} - -/** Pedagogical density: distinct node types relative to total. */ -export function densityScore(detail: DetailResponse): number { - const nodes = detail.data.attributes.workflow.nodes ?? []; - const realNodes = nodes.filter((n) => !isStickyNote(n)); - if (realNodes.length === 0) return 0; - const distinctTypes = new Set(realNodes.map((n) => String(n.type ?? ''))).size; - return Math.min(1, distinctTypes / realNodes.length); -} - -// --------------------------------------------------------------------------- -// Public scorers (compose per-dimension scores into totals) -// --------------------------------------------------------------------------- - -/** Catalog-stage score. Does not include coverage, clarity, density (need detail). */ -export function scoreCatalogEntry(entry: CatalogEntry): ScoreResult { - const traction = tractionScore(entry.totalViews ?? 0, 0); // recentViews unavailable here - // List endpoint only returns createdAt; detail stage rescores with updatedAt. - const recency = recencyScore(entry.createdAt); - const breakdown: RubricBreakdown = { - traction, - recency, - coverage: 0, - aiAgent: 0, - clarity: 0, - density: 0, - }; - const total = WEIGHTS.traction * traction + WEIGHTS.recency * recency; - return { total, breakdown }; -} - -/** Detail-stage score. Full rubric. `runningSet` lets coverage update as picks accumulate. */ -export function scoreDetailedTemplate( - entry: CatalogEntry, - detail: DetailResponse, - runningSet: BucketKey[], -): ScoreResult { - const attrs = detail.data.attributes; - const traction = tractionScore(attrs.views ?? 0, attrs.recentViews ?? 0); - const recency = recencyScore(attrs.updatedAt); - const bucket = bucketKey(detail); - const coverage = coverageScore(bucket, runningSet); - const aiAgent = aiAgentScore(detail); - const clarity = clarityScore(detail); - const density = densityScore(detail); - - const breakdown: RubricBreakdown = { traction, recency, coverage, aiAgent, clarity, density }; - const total = - WEIGHTS.traction * traction + - WEIGHTS.recency * recency + - WEIGHTS.coverage * coverage + - WEIGHTS.aiAgent * aiAgent + - WEIGHTS.clarity * clarity + - WEIGHTS.density * density; - - return { total, breakdown }; -} - -// --------------------------------------------------------------------------- -// Bucket key derivation -// --------------------------------------------------------------------------- - -export function bucketKey(detail: DetailResponse): BucketKey { - return { - triggerType: classifyTrigger(detail), - primaryIntegration: classifyIntegration(detail), - hasAI: hasAI(detail), - controlFlowKind: classifyControlFlow(detail), - }; -} - -export function bucketKeyToString(b: BucketKey): string { - return `${b.triggerType}|${b.primaryIntegration}|${b.hasAI ? 'ai' : 'noai'}|${b.controlFlowKind}`; -} - -function classifyTrigger(detail: DetailResponse): TriggerType { - const node = findTriggerNode(detail); - if (!node) return 'other'; - const type = String(node.type ?? ''); - if (type.includes('chatTrigger')) return 'chatTrigger'; - if (type.includes('webhook')) return 'webhook'; - if (type.includes('scheduleTrigger') || type.includes('cron')) return 'schedule'; - if (type.includes('formTrigger')) return 'formTrigger'; - if (type.includes('manualTrigger')) return 'manual'; - if (type.includes('telegram')) return 'telegram'; - if (type.includes('gmail')) return 'gmail'; - return 'other'; -} - -function classifyIntegration(detail: DetailResponse): string { - const counts = new Map(); - for (const node of detail.data.attributes.workflow.nodes ?? []) { - const t = String(node.type ?? ''); - if (!t) continue; - // Skip triggers and meta nodes from primary integration calc - if (isTriggerType(t) || isMetaType(t)) continue; - const integ = vendorOf(t); - counts.set(integ, (counts.get(integ) ?? 0) + 1); - } - if (counts.size === 0) return 'none'; - const sorted = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]); - return sorted[0][0]; -} - -function classifyControlFlow(detail: DetailResponse): ControlFlowKind { - const nodes = detail.data.attributes.workflow.nodes ?? []; - const types = nodes.map((n) => String(n.type ?? '')); - if (types.some((t) => t.includes('splitInBatches'))) return 'loop'; - if (types.some((t) => t.includes('.if') || t.includes('.switch'))) return 'branching'; - const connections = detail.data.attributes.workflow.connections ?? {}; - for (const fromNode of Object.values(connections)) { - const main = (fromNode as { main?: unknown[][] }).main; - if (Array.isArray(main) && main.length > 1) return 'parallel'; - if (Array.isArray(main) && main[0] && Array.isArray(main[0]) && main[0].length > 1) { - return 'parallel'; - } - } - return 'linear'; -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function findTriggerNode(detail: DetailResponse): Record | null { - for (const node of detail.data.attributes.workflow.nodes ?? []) { - const type = String(node.type ?? ''); - if (isTriggerType(type)) return node; - } - return null; -} - -function isTriggerType(type: string): boolean { - return ( - type.includes('Trigger') || - type.endsWith('.webhook') || - type.endsWith('.cron') || - type.endsWith('.manualTrigger') - ); -} - -function isMetaType(type: string): boolean { - return type.endsWith('.stickyNote') || type.endsWith('.noOp'); -} - -function isStickyNote(node: Record): boolean { - return String(node.type ?? '').endsWith('.stickyNote'); -} - -/** - * Names n8n auto-assigns when a node is added. Numeric suffixes (`Edit Fields1`, - * `HTTP Request2`) are stripped before lookup. Curated; extend as new common - * defaults surface. - */ -const KNOWN_DEFAULT_NAMES = new Set([ - 'Edit Fields', - 'HTTP Request', - 'Schedule Trigger', - 'Manual Trigger', - 'Webhook', - 'Form Trigger', - 'Email Trigger (IMAP)', - 'When chat message received', - 'Sticky Note', - 'AI Agent', - 'Basic LLM Chain', - 'OpenAI Chat Model', - 'Simple Memory', - 'Window Buffer Memory', -]); - -/** - * Heuristic: is this node's name auto-generated by n8n rather than authored? - * - * Two flavours of default: - * - A single Title-Case word optionally followed by a digit: `Set`, `Code`, - * `Slack2`, `Gmail`. These are generated from the node's display name. - * - A known multi-word default in `KNOWN_DEFAULT_NAMES`. - * - * Custom names typically break out of these patterns by adding lowercase words - * (`Post daily update`), specific personalisation (`My Slack post`), or by - * being longer than the canonical default form. - */ -function isDefaultName(node: Record): boolean { - const name = String(node.name ?? '').trim(); - if (!name) return true; - const stripped = name.replace(/\d+$/, '').trim(); - if (KNOWN_DEFAULT_NAMES.has(stripped)) return true; - // Single Title-Case word: 'Set', 'Slack', 'Gmail' - if (/^[A-Z][a-zA-Z]*$/.test(stripped)) return true; - return false; -} - -function vendorOf(type: string): string { - // '@n8n/n8n-nodes-langchain.openAi' → 'langchain' - // 'n8n-nodes-base.googleSheets' → 'googleSheets' - if (type.startsWith('@n8n/n8n-nodes-langchain.')) return 'langchain'; - const dot = type.lastIndexOf('.'); - if (dot < 0) return type; - return type.slice(dot + 1); -} - -export function hasAI(detail: DetailResponse): boolean { - for (const node of detail.data.attributes.workflow.nodes ?? []) { - const t = String(node.type ?? ''); - if (t.startsWith('@n8n/n8n-nodes-langchain.')) return true; - if (t.includes('openAi') || t.includes('anthropic')) return true; - } - return false; -} - -function hasSubnodePattern(detail: DetailResponse): boolean { - const types = (detail.data.attributes.workflow.nodes ?? []).map((n) => String(n.type ?? '')); - const hasAgent = types.some((t) => t.endsWith('.agent')); - const hasMemory = types.some((t) => t.includes('memory')); - const hasTool = types.some((t) => t.includes('tool')); - return hasAgent && (hasMemory || hasTool); -} - -// --------------------------------------------------------------------------- -// Exports for tests -// --------------------------------------------------------------------------- - -export const __test = { - classifyTrigger, - classifyIntegration, - classifyControlFlow, - findTriggerNode, - isDefaultName, - vendorOf, -}; diff --git a/packages/@n8n/workflow-sdk/scripts/extract-workflows.ts b/packages/@n8n/workflow-sdk/scripts/extract-workflows.ts index 5db42a52e59..c4adc47ddeb 100644 --- a/packages/@n8n/workflow-sdk/scripts/extract-workflows.ts +++ b/packages/@n8n/workflow-sdk/scripts/extract-workflows.ts @@ -1,9 +1,7 @@ /** - * Extract Test Workflows from Zips + * Extract Test Workflows from the test-fixtures zip. * - * Extracts workflow JSON files from committed zip files for testing: - * - test-fixtures/real-workflows/public_published_templates.zip → test-fixtures/real-workflows/ - * - examples/templates.zip → examples/workflows/ (used by examples-roundtrip test) + * test-fixtures/real-workflows/public_published_templates.zip → test-fixtures/real-workflows/ * * Runs automatically via Jest's globalSetup (see scripts/jest-global-setup.ts). * @@ -18,10 +16,6 @@ import AdmZip from 'adm-zip'; const FIXTURES_DIR = path.resolve(__dirname, '../test-fixtures/real-workflows'); const FIXTURES_ZIP = path.join(FIXTURES_DIR, 'public_published_templates.zip'); -const EXAMPLES_DIR = path.resolve(__dirname, '../examples'); -const EXAMPLES_ZIP = path.join(EXAMPLES_DIR, 'templates.zip'); -const EXAMPLES_WORKFLOWS_DIR = path.join(EXAMPLES_DIR, 'workflows'); - function extractZip(zipPath: string, outputDir: string, label: string) { if (!fs.existsSync(zipPath)) { console.log(`No ${label} zip found at ${zipPath}, skipping extraction`); @@ -58,7 +52,6 @@ function extractZip(zipPath: string, outputDir: string, label: string) { export function extractAllWorkflows() { extractZip(FIXTURES_ZIP, FIXTURES_DIR, 'test-fixtures workflows'); - extractZip(EXAMPLES_ZIP, EXAMPLES_WORKFLOWS_DIR, 'examples workflows'); } if (require.main === module) { diff --git a/packages/@n8n/workflow-sdk/scripts/fetch-templates.ts b/packages/@n8n/workflow-sdk/scripts/fetch-templates.ts deleted file mode 100644 index 37bd70479f5..00000000000 --- a/packages/@n8n/workflow-sdk/scripts/fetch-templates.ts +++ /dev/null @@ -1,230 +0,0 @@ -/** - * Fetch n8n.io public template catalog for criteria-driven curation. - * - * Two passes: - * Pass 1 (list) – paginate the templates list endpoint, persist each page, - * concat into examples/_raw/_catalog.json. Cheap metadata only. - * Pass 2 (detail) – per-id fetch of the full workflow JSON. Called by - * regenerate-examples.ts only for candidates the rubric picks. - * - * Usage: - * pnpm fetch-templates # full catalog walk (pass 1) - * pnpm fetch-templates --rebuild # ignore cached pages, re-walk - * - * Both passes are resumable: existing files are skipped on rerun. - */ -import * as fs from 'fs'; -import * as path from 'path'; - -const RAW_DIR = path.resolve(__dirname, '../examples/_raw'); -const LIST_DIR = path.join(RAW_DIR, '_list'); -const CATALOG_FILE = path.join(RAW_DIR, '_catalog.json'); -const DETAIL_DIR = RAW_DIR; - -// Search endpoint paginates correctly (api.n8n.io/api/templates/workflows ignores ?page=). -// Mirrors the working pattern in scripts/fetch-test-workflows.ts. -const LIST_URL = 'https://n8n.io/api/product-api/workflows/search'; -const DETAIL_URL = 'https://api.n8n.io/api/workflows'; - -const ROWS_PER_PAGE = 200; -const RATE_LIMIT_MS = 200; // ~5 req/s -const MAX_RETRIES = 3; -const FETCH_TIMEOUT_MS = 30_000; - -export interface CatalogEntry { - id: number; - name: string; - description?: string; - totalViews: number; - createdAt: string; - user: { username: string; verified: boolean }; - price?: number | null; - purchaseUrl: string | null; - nodes: Array<{ name: string; group?: string; displayName?: string }>; -} - -interface ListResponse { - totalWorkflows: number; - workflows: CatalogEntry[]; -} - -export interface DetailResponse { - data: { - id: number; - attributes: { - name: string; - description: string; - workflow: { - nodes: Array>; - connections: Record; - meta?: Record; - pinData?: Record; - settings?: Record; - }; - workflowInfo?: { - nodeCount: number; - nodeTypes: Record; - }; - createdAt: string; - updatedAt: string; - views: number; - recentViews: number; - hidden: boolean; - username: string; - status: string; - price: number | null; - difficulty: string | null; - readyToDemo: boolean | null; - }; - }; - meta: Record; -} - -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -async function politeFetch(url: string, attempt = 1): Promise { - try { - const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); - if (response.status === 429 || response.status >= 500) { - if (attempt > MAX_RETRIES) { - console.error(` Giving up on ${url} after ${MAX_RETRIES} retries (${response.status})`); - return null; - } - const backoff = 2 ** attempt * 1000; - console.log( - ` ${response.status} from ${url}, backing off ${backoff}ms (attempt ${attempt})`, - ); - await sleep(backoff); - return politeFetch(url, attempt + 1); - } - if (!response.ok) { - console.error(` ${response.status} ${response.statusText}: ${url}`); - return null; - } - return (await response.json()) as T; - } catch (error) { - if (attempt > MAX_RETRIES) { - console.error(` Network error on ${url} after ${MAX_RETRIES} retries:`, error); - return null; - } - const backoff = 2 ** attempt * 1000; - console.log(` Network error on ${url}, backing off ${backoff}ms (attempt ${attempt})`); - await sleep(backoff); - return politeFetch(url, attempt + 1); - } -} - -function ensureDirs() { - for (const dir of [RAW_DIR, LIST_DIR]) { - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - } -} - -function pagePath(page: number) { - return path.join(LIST_DIR, `page-${String(page).padStart(4, '0')}.json`); -} - -export async function fetchCatalog(opts: { rebuild?: boolean } = {}): Promise { - ensureDirs(); - - console.log('Pass 1: walking template catalog...\n'); - const allEntries: CatalogEntry[] = []; - let page = 1; - let totalExpected: number | undefined; - - while (true) { - const cachePath = pagePath(page); - let response: ListResponse | null; - - if (!opts.rebuild && fs.existsSync(cachePath)) { - response = JSON.parse(fs.readFileSync(cachePath, 'utf-8')) as ListResponse; - console.log(` Page ${page}: ${response.workflows.length} workflows (cached)`); - } else { - const url = `${LIST_URL}?page=${page}&rows=${ROWS_PER_PAGE}`; - response = await politeFetch(url); - if (response === null) { - // Fail fast: writing a partial catalog would look valid on disk - // but silently drop later pages, masking the failure. - throw new Error(`Catalog walk failed at page ${page}; refusing to write partial catalog`); - } - fs.writeFileSync(cachePath, JSON.stringify(response, null, 2)); - console.log( - ` Page ${page}: ${response.workflows.length} workflows (fetched, total=${response.totalWorkflows})`, - ); - await sleep(RATE_LIMIT_MS); - } - - if (totalExpected === undefined) totalExpected = response.totalWorkflows; - if (response.workflows.length === 0) break; - allEntries.push(...response.workflows); - if (allEntries.length >= response.totalWorkflows) break; - page++; - } - - const dedupedById = new Map(); - for (const entry of allEntries) dedupedById.set(entry.id, entry); - const catalog = Array.from(dedupedById.values()); - - fs.writeFileSync(CATALOG_FILE, JSON.stringify(catalog, null, 2)); - console.log( - `\nWrote ${catalog.length} unique entries to ${path.relative(process.cwd(), CATALOG_FILE)} (server total=${totalExpected})\n`, - ); - - return catalog; -} - -function detailPath(id: number) { - return path.join(DETAIL_DIR, `${id}.json`); -} - -export async function fetchDetail( - id: number, - opts: { force?: boolean } = {}, -): Promise { - ensureDirs(); - const cachePath = detailPath(id); - - if (!opts.force && fs.existsSync(cachePath)) { - try { - return JSON.parse(fs.readFileSync(cachePath, 'utf-8')) as DetailResponse; - } catch { - // fall through to refetch - } - } - - const response = await politeFetch(`${DETAIL_URL}/${id}`); - if (response === null) return null; - fs.writeFileSync(cachePath, JSON.stringify(response, null, 2)); - await sleep(RATE_LIMIT_MS); - return response; -} - -export function loadCachedCatalog(): CatalogEntry[] { - if (!fs.existsSync(CATALOG_FILE)) { - throw new Error(`Catalog not found at ${CATALOG_FILE}. Run \`pnpm fetch-templates\` first.`); - } - return JSON.parse(fs.readFileSync(CATALOG_FILE, 'utf-8')) as CatalogEntry[]; -} - -export function loadCachedDetail(id: number): DetailResponse | null { - const p = detailPath(id); - if (!fs.existsSync(p)) return null; - try { - return JSON.parse(fs.readFileSync(p, 'utf-8')) as DetailResponse; - } catch { - return null; - } -} - -async function main() { - const rebuild = process.argv.includes('--rebuild'); - const catalog = await fetchCatalog({ rebuild }); - console.log(`Done. Catalog has ${catalog.length} entries.`); -} - -if (require.main === module) { - main().catch((error) => { - console.error(error); - process.exit(1); - }); -} diff --git a/packages/@n8n/workflow-sdk/scripts/regenerate-examples.ts b/packages/@n8n/workflow-sdk/scripts/regenerate-examples.ts deleted file mode 100644 index fa508aa3050..00000000000 --- a/packages/@n8n/workflow-sdk/scripts/regenerate-examples.ts +++ /dev/null @@ -1,551 +0,0 @@ -/** - * Regenerate `examples/manifest.json`, `examples/workflows/*.json`, and - * `examples/templates.zip` from the cached public-template catalog. - * - * The goal is a small, diverse set of high-quality real workflows the builder - * agent can grep when constructing new ones. Diversity beats raw popularity: - * we'd rather have one good `webhook + Slack + AI + branching` example than - * ten of them. - * - * Stage 1 — Catalog gate + cheap score (`scoreCatalogEntry`) - * For every entry in the cached catalog, drop paid templates, unverified - * authors, and oversized workflows. Score survivors on `traction` (log-scaled - * views) and `recency` (linear decay from 90→730 days). By default the full - * survivor set is used; pass `--candidates=N` to cap the detail-fetch budget - * on a cold cache. - * - * Stage 2 — Detail fetch + full-rubric score (`scoreDetailedTemplate`) - * For each candidate, fetch its detail JSON (cached), re-gate on real node - * count + trigger presence, compute its bucket key - * `(triggerType, primaryIntegration, hasAI, controlFlowKind)`, and score on - * the full 6-dimension rubric (traction, recency, coverage, aiAgent, clarity, - * density). Weights live in `criteria.ts` — `coverage` is the dominant - * weight (35) because it drives bucket diversity. - * - * Stage 3 — Greedy round-robin pick - * Loop until `--target` (default 50) accepted: re-score every remaining - * candidate against the *current* `acceptedBuckets` (coverage decays once a - * bucket fills), pick the highest scorer, validate it round-trips through - * `generateWorkflowCode` + `emitInstanceAi`, accept on success. Validation - * failures are logged to `_failures.log` and the candidate is dropped. - * - * Stage 3b — Coverage patch for must-cover node types - * `MUST_COVER_NODE_TYPES` (Postgres + all langchain vector stores) must each - * appear somewhere in the manifest. For any missing type, first scan the - * 1000-candidate pool; if none has it, fall back to scanning the full - * catalog and fetching detail on demand for up to 25 ranked candidates. - * This is why the final count usually exceeds `--target`. - * - * Stage 4 — Write outputs - * Clear `examples/workflows/`, write one JSON per accepted candidate, write - * `manifest.json` sorted by score (with the per-dimension breakdown so picks - * are reviewable), write `_catalog-snapshot.json`, then pack the workflow - * JSONs into `examples/templates.zip`. Only the manifest and the zip are - * committed; the unpacked JSONs are gitignored and recreated on demand by - * `examples-loader`. - * - * Usage: - * pnpm regenerate-examples # default target 50 - * pnpm regenerate-examples --target=100 # explicit target - * pnpm regenerate-examples --candidates=2000 # cap detail-fetch budget - */ -import AdmZip from 'adm-zip'; -import * as fs from 'fs'; -import * as path from 'path'; - -import { generateWorkflowCode, emitInstanceAi } from '../src/codegen'; -import { - bucketKey, - bucketKeyToString, - mechanicalGateCatalog, - mechanicalGateDetail, - scoreCatalogEntry, - scoreDetailedTemplate, - type BucketKey, - type ScoreResult, -} from './criteria'; -import { - fetchDetail, - loadCachedCatalog, - type CatalogEntry, - type DetailResponse, -} from './fetch-templates'; - -const EXAMPLES_DIR = path.resolve(__dirname, '../examples'); -const WORKFLOWS_DIR = path.join(EXAMPLES_DIR, 'workflows'); -const MANIFEST_PATH = path.join(EXAMPLES_DIR, 'manifest.json'); -const ZIP_PATH = path.join(EXAMPLES_DIR, 'templates.zip'); -const SNAPSHOT_PATH = path.join(EXAMPLES_DIR, '_catalog-snapshot.json'); -const FAILURES_LOG = path.join(EXAMPLES_DIR, '_failures.log'); - -const DEFAULT_TARGET = 50; -// No cap by default — process the full catalog. Detail JSONs are cached on -// disk in `examples/_raw/`, so warm runs are cheap. Pass `--candidates=N` to -// bound the detail-fetch budget on a cold cache. -const DEFAULT_CANDIDATES = Infinity; - -/** - * Popular catalog node types that must be represented in the manifest. After - * the main bucket-pick loop, any of these missing from the manifest gets a - * coverage patch: we force-include the highest-scoring eligible candidate - * containing that type. Extends the manifest beyond `target`. - * - * If a must-cover type's candidates didn't make the top-K catalog cohort, the - * patch step falls back to fetching detail for the type's highest-scoring - * catalog entries on demand. - */ -const MUST_COVER_NODE_TYPES = [ - 'n8n-nodes-base.postgres', - '@n8n/n8n-nodes-langchain.vectorStorePinecone', - '@n8n/n8n-nodes-langchain.vectorStoreSupabase', - '@n8n/n8n-nodes-langchain.vectorStoreQdrant', - '@n8n/n8n-nodes-langchain.vectorStoreInMemory', - '@n8n/n8n-nodes-langchain.vectorStorePGVector', - '@n8n/n8n-nodes-langchain.vectorStoreMongoDBAtlas', - '@n8n/n8n-nodes-langchain.vectorStoreWeaviate', - '@n8n/n8n-nodes-langchain.vectorStoreMilvus', - '@n8n/n8n-nodes-langchain.vectorStoreRedis', -] as const; - -interface CliArgs { - target: number; - candidates: number; -} - -interface ManifestEntry { - id: number; - slug: string; - name: string; - description: string; - nodes: string[]; - tags: string[]; - triggerType: string; - hasAI: boolean; - score: number; - scoreBreakdown: ScoreResult['breakdown']; - source: string; - author: string; - success: true; -} - -interface SnapshotEntry { - id: number; - name: string; - score: number; - createdAt: string; - totalViews: number; - picked: boolean; - dropReason?: string; -} - -function parseArgs(): CliArgs { - const args = { target: DEFAULT_TARGET, candidates: DEFAULT_CANDIDATES }; - for (const a of process.argv.slice(2)) { - const [k, v] = a.split('='); - if (k === '--target') args.target = Number(v); - else if (k === '--candidates') args.candidates = Number(v); - } - return args; -} - -function ensureDirs() { - for (const dir of [EXAMPLES_DIR, WORKFLOWS_DIR]) { - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - } -} - -function clearFailuresLog() { - if (fs.existsSync(FAILURES_LOG)) fs.unlinkSync(FAILURES_LOG); -} - -function logFailure(id: number, name: string, reason: string) { - fs.appendFileSync(FAILURES_LOG, `${id} | ${name} | ${reason}\n`); -} - -function makeSlug(id: number, name: string): string { - const base = name - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-|-$/g, '') - .slice(0, 60); - return `${base || 'workflow'}-${id}`; -} - -function detailToWorkflowJson(detail: DetailResponse) { - const attrs = detail.data.attributes; - return { - id: `wf-${detail.data.id}`, - name: attrs.name, - nodes: attrs.workflow.nodes, - connections: attrs.workflow.connections, - settings: attrs.workflow.settings ?? {}, - pinData: attrs.workflow.pinData ?? {}, - }; -} - -function buildTags(detail: DetailResponse, key: BucketKey): string[] { - const tags: string[] = [`trigger:${key.triggerType}`]; - if (key.hasAI) tags.push('ai'); - if (key.primaryIntegration && key.primaryIntegration !== 'none') { - tags.push(`integration:${key.primaryIntegration}`); - } - return tags; -} - -function uniqueNodeTypes(detail: DetailResponse): string[] { - const seen = new Set(); - for (const node of detail.data.attributes.workflow.nodes ?? []) { - const type = String(node.type ?? ''); - if (type) seen.add(type); - } - return Array.from(seen); -} - -function validateRoundtrip(detail: DetailResponse): { ok: true } | { ok: false; reason: string } { - const json = detailToWorkflowJson(detail); - let firstPass: string; - try { - firstPass = generateWorkflowCode(json); - } catch (error) { - return { ok: false, reason: `codegen threw: ${(error as Error).message}` }; - } - if (!firstPass || firstPass.length === 0) { - return { ok: false, reason: 'codegen produced empty output' }; - } - try { - const wrapped = emitInstanceAi(json); - if (!wrapped.includes("from '@n8n/workflow-sdk'")) { - return { ok: false, reason: 'emitInstanceAi produced output without SDK import' }; - } - } catch (error) { - return { ok: false, reason: `emitInstanceAi threw: ${(error as Error).message}` }; - } - return { ok: true }; -} - -interface ScoredCandidate { - entry: CatalogEntry; - detail: DetailResponse; - bucket: BucketKey; - bucketStr: string; - scoreAtPick: ScoreResult; -} - -async function main() { - const args = parseArgs(); - ensureDirs(); - clearFailuresLog(); - - const candidatesLabel = Number.isFinite(args.candidates) ? args.candidates : 'all'; - console.log(`Regenerating examples (target=${args.target}, candidates=${candidatesLabel})\n`); - - // Stage 1: catalog-stage filter and score - const catalog = loadCachedCatalog(); - console.log(`Loaded ${catalog.length} catalog entries`); - - const snapshot: SnapshotEntry[] = []; - const survivors: Array<{ entry: CatalogEntry; score: number }> = []; - - for (const entry of catalog) { - const gate = mechanicalGateCatalog(entry); - if (!gate.ok) { - snapshot.push({ - id: entry.id, - name: entry.name, - score: 0, - createdAt: entry.createdAt, - totalViews: entry.totalViews, - picked: false, - dropReason: gate.reason, - }); - continue; - } - const s = scoreCatalogEntry(entry); - survivors.push({ entry, score: s.total }); - snapshot.push({ - id: entry.id, - name: entry.name, - score: s.total, - createdAt: entry.createdAt, - totalViews: entry.totalViews, - picked: false, - }); - } - - survivors.sort((a, b) => b.score - a.score); - const topCandidates = survivors.slice(0, args.candidates); - console.log( - `Stage 1: ${survivors.length} catalog survivors → top ${topCandidates.length} for detail fetch`, - ); - - // Stage 2: fetch detail and full-rubric score - const scored: ScoredCandidate[] = []; - let detailFetched = 0; - let detailDropped = 0; - let detailFailed = 0; - - for (let i = 0; i < topCandidates.length; i++) { - const { entry } = topCandidates[i]; - if (i % 50 === 0) { - console.log(` detail fetch ${i}/${topCandidates.length}...`); - } - const detail = await fetchDetail(entry.id); - if (!detail) { - detailFailed++; - logFailure(entry.id, entry.name, 'detail fetch failed'); - continue; - } - detailFetched++; - const gate = mechanicalGateDetail(detail); - if (!gate.ok) { - detailDropped++; - logFailure(entry.id, entry.name, `detail gate: ${gate.reason}`); - continue; - } - const bucket = bucketKey(detail); - // score with empty running set; we'll re-score during bucket pick - const s = scoreDetailedTemplate(entry, detail, []); - scored.push({ - entry, - detail, - bucket, - bucketStr: bucketKeyToString(bucket), - scoreAtPick: s, - }); - } - - console.log( - `Stage 2: detail fetched=${detailFetched}, dropped=${detailDropped}, failed=${detailFailed}, scored=${scored.length}`, - ); - - // Stage 3: greedy round-robin pick by bucket count, recomputing coverage - const accepted: ScoredCandidate[] = []; - const acceptedBuckets: BucketKey[] = []; - - while (accepted.length < args.target) { - let best: { idx: number; score: number; cand: ScoredCandidate } | null = null; - for (let i = 0; i < scored.length; i++) { - const cand = scored[i]; - if (accepted.includes(cand)) continue; - const fresh = scoreDetailedTemplate(cand.entry, cand.detail, acceptedBuckets); - if (best === null || fresh.total > best.score) { - best = { idx: i, score: fresh.total, cand }; - } - } - if (best === null) break; - - // Validate before accepting - const valid = validateRoundtrip(best.cand.detail); - if (!valid.ok) { - scored.splice(best.idx, 1); - logFailure(best.cand.entry.id, best.cand.entry.name, `validation: ${valid.reason}`); - continue; - } - - const fresh = scoreDetailedTemplate(best.cand.entry, best.cand.detail, acceptedBuckets); - best.cand.scoreAtPick = fresh; - accepted.push(best.cand); - acceptedBuckets.push(best.cand.bucket); - scored.splice(best.idx, 1); - } - - console.log(`Stage 3: accepted ${accepted.length} workflows after validation`); - - // Stage 3b: coverage patch — force-include must-cover node types missing from accepted - const acceptedTypes = new Set(); - for (const cand of accepted) { - for (const node of cand.detail.data.attributes.workflow.nodes ?? []) { - acceptedTypes.add(String(node.type ?? '')); - } - } - let patchedCount = 0; - for (const mustType of MUST_COVER_NODE_TYPES) { - if (acceptedTypes.has(mustType)) continue; - - // First-tier: try scored candidates already in our pool. - const fromScored = scored - .map((cand) => { - const types = (cand.detail.data.attributes.workflow.nodes ?? []).map((n) => - String(n.type ?? ''), - ); - if (!types.includes(mustType)) return null; - const fresh = scoreDetailedTemplate(cand.entry, cand.detail, acceptedBuckets); - return { cand, score: fresh.total }; - }) - .filter((x): x is { cand: ScoredCandidate; score: number } => x !== null) - .sort((a, b) => b.score - a.score); - - let added = false; - for (const { cand, score } of fromScored) { - const valid = validateRoundtrip(cand.detail); - if (!valid.ok) { - logFailure(cand.entry.id, cand.entry.name, `coverage-patch validation: ${valid.reason}`); - continue; - } - const fresh = scoreDetailedTemplate(cand.entry, cand.detail, acceptedBuckets); - cand.scoreAtPick = fresh; - accepted.push(cand); - acceptedBuckets.push(cand.bucket); - scored.splice(scored.indexOf(cand), 1); - for (const node of cand.detail.data.attributes.workflow.nodes ?? []) { - acceptedTypes.add(String(node.type ?? '')); - } - console.log( - ` coverage patch (+1 for ${mustType}): id=${cand.entry.id} score=${score.toFixed(2)} ${cand.entry.name.slice(0, 60)}`, - ); - patchedCount++; - added = true; - break; - } - if (added) continue; - - // Second-tier: type wasn't in the top-K candidate pool. Scan the full - // catalog for entries whose sparse list contains the type, fetch detail - // on demand (cached after first hit), and accept the first that passes - // gate + roundtrip. Ranked by catalog-stage score so we try the - // strongest candidate first. - const catalogCandidates = catalog - .filter((entry) => (entry.nodes ?? []).some((n) => n.name === mustType)) - .filter((entry) => mechanicalGateCatalog(entry).ok) - .map((entry) => ({ entry, score: scoreCatalogEntry(entry).total })) - .sort((a, b) => b.score - a.score) - .slice(0, 25); // bounded — don't go fetching the whole tail - - for (const { entry } of catalogCandidates) { - const detail = await fetchDetail(entry.id); - if (!detail) continue; - if (!mechanicalGateDetail(detail).ok) continue; - // Catalog `entry.nodes` is a sparse list that can drift from the - // real workflow JSON; re-verify against the detail before accepting. - const detailHasType = (detail.data.attributes.workflow.nodes ?? []).some( - (n) => String(n.type ?? '') === mustType, - ); - if (!detailHasType) continue; - const valid = validateRoundtrip(detail); - if (!valid.ok) { - logFailure(entry.id, entry.name, `coverage-patch fallback validation: ${valid.reason}`); - continue; - } - const bucket = bucketKey(detail); - const fresh = scoreDetailedTemplate(entry, detail, acceptedBuckets); - const cand: ScoredCandidate = { - entry, - detail, - bucket, - bucketStr: bucketKeyToString(bucket), - scoreAtPick: fresh, - }; - accepted.push(cand); - acceptedBuckets.push(bucket); - for (const node of detail.data.attributes.workflow.nodes ?? []) { - acceptedTypes.add(String(node.type ?? '')); - } - console.log( - ` coverage patch fallback (+1 for ${mustType}): id=${entry.id} score=${fresh.total.toFixed(2)} ${entry.name.slice(0, 60)}`, - ); - patchedCount++; - break; - } - } - if (patchedCount > 0) { - console.log(`Stage 3b: coverage patch added ${patchedCount} workflows`); - } - console.log(); - - // Stage 4: write workflows + manifest - // Clear existing committed workflow files - for (const f of fs.readdirSync(WORKFLOWS_DIR)) { - if (f.endsWith('.json')) fs.unlinkSync(path.join(WORKFLOWS_DIR, f)); - } - - const manifestEntries: ManifestEntry[] = []; - const slugSet = new Set(); - for (const cand of accepted) { - const baseSlug = makeSlug(cand.entry.id, cand.entry.name); - let slug = baseSlug; - let suffix = 2; - while (slugSet.has(slug)) slug = `${baseSlug}-${suffix++}`; - slugSet.add(slug); - - const wfJson = detailToWorkflowJson(cand.detail); - fs.writeFileSync(path.join(WORKFLOWS_DIR, `${slug}.json`), JSON.stringify(wfJson, null, 2)); - - const tags = buildTags(cand.detail, cand.bucket); - manifestEntries.push({ - id: cand.entry.id, - slug, - name: cand.entry.name, - description: cand.detail.data.attributes.description ?? '', - nodes: uniqueNodeTypes(cand.detail), - tags, - triggerType: cand.bucket.triggerType, - hasAI: cand.bucket.hasAI, - score: Number(cand.scoreAtPick.total.toFixed(2)), - scoreBreakdown: { - traction: Number(cand.scoreAtPick.breakdown.traction.toFixed(3)), - recency: Number(cand.scoreAtPick.breakdown.recency.toFixed(3)), - coverage: Number(cand.scoreAtPick.breakdown.coverage.toFixed(3)), - aiAgent: Number(cand.scoreAtPick.breakdown.aiAgent.toFixed(3)), - clarity: Number(cand.scoreAtPick.breakdown.clarity.toFixed(3)), - density: Number(cand.scoreAtPick.breakdown.density.toFixed(3)), - }, - source: `https://n8n.io/workflows/${cand.entry.id}`, - author: cand.detail.data.attributes.username || cand.entry.user.username || 'unknown', - success: true, - }); - - // Mark in snapshot - const snap = snapshot.find((s) => s.id === cand.entry.id); - if (snap) snap.picked = true; - } - - manifestEntries.sort((a, b) => b.score - a.score); - fs.writeFileSync( - MANIFEST_PATH, - JSON.stringify( - { - generatedAt: new Date().toISOString(), - workflows: manifestEntries, - }, - null, - 2, - ), - ); - - fs.writeFileSync(SNAPSHOT_PATH, JSON.stringify(snapshot, null, 2)); - - // Pack workflow JSONs into the committed zip so the unpacked dir can be gitignored. - const zip = new AdmZip(); - for (const entry of manifestEntries) { - zip.addLocalFile(path.join(WORKFLOWS_DIR, `${entry.slug}.json`)); - } - zip.writeZip(ZIP_PATH); - - // Bucket distribution report - const bucketDistribution = new Map(); - for (const e of manifestEntries) { - const k = `${e.triggerType}|${e.hasAI ? 'ai' : 'noai'}`; - bucketDistribution.set(k, (bucketDistribution.get(k) ?? 0) + 1); - } - console.log('Bucket distribution (triggerType × hasAI):'); - for (const [k, v] of Array.from(bucketDistribution.entries()).sort((a, b) => b[1] - a[1])) { - console.log(` ${v.toString().padStart(3)} | ${k}`); - } - console.log(); - - console.log( - `Wrote ${manifestEntries.length} entries to ${path.relative(process.cwd(), MANIFEST_PATH)}`, - ); - console.log(`Wrote workflow JSONs to ${path.relative(process.cwd(), WORKFLOWS_DIR)}/`); - console.log(`Wrote zip to ${path.relative(process.cwd(), ZIP_PATH)}`); - console.log(`Catalog snapshot: ${path.relative(process.cwd(), SNAPSHOT_PATH)}`); - if (fs.existsSync(FAILURES_LOG)) { - const failuresCount = fs.readFileSync(FAILURES_LOG, 'utf-8').split('\n').filter(Boolean).length; - console.log(`Failures (${failuresCount}): ${path.relative(process.cwd(), FAILURES_LOG)}`); - } -} - -main().catch((error) => { - console.error(error); - process.exit(1); -}); diff --git a/packages/@n8n/workflow-sdk/src/__tests__/examples-roundtrip.test.ts b/packages/@n8n/workflow-sdk/src/__tests__/examples-roundtrip.test.ts deleted file mode 100644 index 75a595c5b94..00000000000 --- a/packages/@n8n/workflow-sdk/src/__tests__/examples-roundtrip.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Roundtrip test for the curated examples set. - * - * For every entry in `examples/manifest.json` (where success !== false), assert: - * - The source JSON loads - * - `emitInstanceAi()` produces non-empty output with the expected SDK import - * - `parseWorkflowCode()` can parse it back, and the parsed node count matches - * - * This is the only CI hook for the examples pipeline. It catches both codegen - * regressions and SDK drift in a single test, mirroring the pattern in - * src/codegen/codegen-roundtrip.test.ts. - */ -import * as fs from 'fs'; -import * as path from 'path'; - -import { emitInstanceAi } from '../codegen/emit-instance-ai'; -import { parseWorkflowCode } from '../codegen/parse-workflow-code'; -import type { WorkflowJSON } from '../types/base'; - -const EXAMPLES_DIR = path.resolve(__dirname, '../../examples'); -const MANIFEST_PATH = path.join(EXAMPLES_DIR, 'manifest.json'); -const WORKFLOWS_DIR = path.join(EXAMPLES_DIR, 'workflows'); - -interface ManifestEntry { - id: number; - slug: string; - name: string; - success: boolean; - skip?: boolean; - skipReason?: string; -} - -function loadManifest(): ManifestEntry[] { - if (!fs.existsSync(MANIFEST_PATH)) return []; - // eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse -- Test fixture file - const raw = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf-8')) as { workflows: ManifestEntry[] }; - return raw.workflows ?? []; -} - -function loadWorkflowJson(slug: string): WorkflowJSON { - const filePath = path.join(WORKFLOWS_DIR, `${slug}.json`); - // eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse -- Test fixture file - return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as WorkflowJSON; -} - -const entries = loadManifest().filter((e) => e.success && !e.skip); - -/** - * `parseWorkflowCode` does not accept ESM import declarations; emit-instance-ai - * adds them. Strip them (and any leading JSDoc header) before parsing. - */ -function stripHeader(code: string): string { - let body = code; - body = body.replace(/^\s*\/\*\*[\s\S]*?\*\/\s*/, ''); - body = body.replace(/^import\s+\{[^}]*\}\s+from\s+'[^']+'\s*;\s*/m, ''); - return body.trimStart(); -} - -// When the manifest is empty (run `pnpm regenerate-examples` to populate), -// `it.each([])` registers no tests, which is the desired behaviour. -describe('examples manifest roundtrip', () => { - it.each(entries.map((e) => [e.slug, e]))( - '%s: emitInstanceAi → parseWorkflowCode roundtrips', - (_slug, entry) => { - const json = loadWorkflowJson(entry.slug); - const code = emitInstanceAi(json); - - expect(code.length).toBeGreaterThan(0); - expect(code).toContain("from '@n8n/workflow-sdk'"); - expect(code).toContain('workflow('); - expect(code).toContain('export default'); - - const body = stripHeader(code); - const parsed = parseWorkflowCode(body); - const sourceNodeCount = json.nodes?.length ?? 0; - const parsedNodeCount = parsed.nodes?.length ?? 0; - // Some normalisation can shift the count by 1 (e.g. sticky note handling). - expect(Math.abs(parsedNodeCount - sourceNodeCount)).toBeLessThanOrEqual(1); - }, - ); -}); diff --git a/packages/@n8n/workflow-sdk/src/examples-loader.test.ts b/packages/@n8n/workflow-sdk/src/examples-loader.test.ts deleted file mode 100644 index c853f8ceb8d..00000000000 --- a/packages/@n8n/workflow-sdk/src/examples-loader.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { getExampleFiles, resetExampleFilesCache } from './examples-loader'; - -describe('examples-loader', () => { - beforeEach(() => resetExampleFilesCache()); - - it('loads manifest entries when present', () => { - const bundle = getExampleFiles(); - // In CI without a manifest, both arrays will be empty — that's also valid. - // When a manifest exists (post `pnpm regenerate-examples`), assert structure. - if (bundle.files.length === 0) { - expect(bundle.indexTxt).toBe(''); - return; - } - - // Each generated file has the expected shape - for (const file of bundle.files) { - expect(file.filename).toMatch(/\.ts$/); - expect(file.content).toContain('@template'); - expect(file.content).toContain("from '@n8n/workflow-sdk'"); - expect(file.content).toContain('export default'); - // Untrusted catalog description must not be embedded in JSDoc. - expect(file.content).not.toContain('@description'); - } - - // Index lines match the file count - const indexLines = bundle.indexTxt.trim().split('\n'); - expect(indexLines.length).toBe(bundle.files.length); - - // Each index line has the documented 5-field shape - for (const line of indexLines) { - const parts = line.split(' | '); - expect(parts.length).toBe(5); - expect(parts[0]).toMatch(/\.ts$/); - expect(parts[4]).toMatch(/^n8n:\d+/); - } - }); - - it('memoises across calls', () => { - const a = getExampleFiles(); - const b = getExampleFiles(); - expect(a).toBe(b); - }); - - it('resetExampleFilesCache forces a reload', () => { - const a = getExampleFiles(); - resetExampleFilesCache(); - const b = getExampleFiles(); - expect(a).not.toBe(b); - }); -}); diff --git a/packages/@n8n/workflow-sdk/src/examples-loader.ts b/packages/@n8n/workflow-sdk/src/examples-loader.ts deleted file mode 100644 index 7abd1bd8d22..00000000000 --- a/packages/@n8n/workflow-sdk/src/examples-loader.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Runtime loader for the curated workflow examples. - * - * Reads `examples/manifest.json` + `examples/workflows/*.json`, runs each - * through `emitInstanceAi` with a JSDoc header pulled from the manifest entry, - * and returns the resulting `.ts` strings plus a flat grep-able `index.txt`. - * - * Used by the instance-ai sandbox-setup to populate `${workspaceRoot}/examples/` - * so the builder agent can grep the index and `cat` matching `.ts` files. - * - * Results are memoised — the manifest is committed and immutable per package - * version, so loading once per process is enough. - */ -import * as fs from 'fs'; -import * as path from 'path'; - -import { emitInstanceAi } from './codegen/emit-instance-ai'; -import { ensureExtracted, WORKFLOWS_CACHE_DIR } from './examples-zip'; -import type { WorkflowJSON } from './types/base'; - -// Manifest ships read-only in the package; workflows live in WORKFLOWS_CACHE_DIR. -const EXAMPLES_DIR = path.resolve(__dirname, '..', 'examples'); -const MANIFEST_PATH = path.join(EXAMPLES_DIR, 'manifest.json'); - -const NODES_INLINE_LIMIT = 5; -const INDEX_NODE_SEPARATOR = ','; - -export interface ExampleFile { - /** Filename relative to `examples/` (e.g. `slack-daily-summary.ts`). */ - filename: string; - /** Full file content: optional JSDoc header, single SDK import, workflow body. */ - content: string; -} - -export interface ExampleFilesBundle { - /** One generated `.ts` string per manifest entry, in score-descending order. */ - files: ExampleFile[]; - /** Flat grep-able index, one line per template, sorted by score descending. */ - indexTxt: string; -} - -interface ManifestEntry { - id: number; - slug: string; - name: string; - description: string; - nodes: string[]; - tags: string[]; - triggerType: string; - hasAI: boolean; - score: number; - source: string; - author: string; - success: boolean; - skip?: boolean; -} - -interface ManifestFile { - workflows: ManifestEntry[]; -} - -let cached: ExampleFilesBundle | null = null; - -/** - * Load and prepare the curated examples for sandbox use. Memoised per process. - * - * Returns an empty bundle if the manifest does not exist (e.g. the consumer is - * running against an unfetched workspace). Sandbox-setup checks for an empty - * bundle and skips the write. - */ -export function getExampleFiles(): ExampleFilesBundle { - if (cached !== null) return cached; - cached = loadFromDisk(); - return cached; -} - -/** Reset the memoisation cache. Tests use this; production callers should not. */ -export function resetExampleFilesCache(): void { - cached = null; -} - -function loadFromDisk(): ExampleFilesBundle { - if (!fs.existsSync(MANIFEST_PATH)) return { files: [], indexTxt: '' }; - ensureExtracted(); - - // eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse -- Internal manifest file - const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf-8')) as ManifestFile; - const entries = (manifest.workflows ?? []) - .filter((e) => e.success && !e.skip) - .sort((a, b) => b.score - a.score); - - const files: ExampleFile[] = []; - const indexLines: string[] = []; - - for (const entry of entries) { - const wfPath = path.join(WORKFLOWS_CACHE_DIR, `${entry.slug}.json`); - if (!fs.existsSync(wfPath)) continue; - // eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse -- Internal workflow fixture - const wf = JSON.parse(fs.readFileSync(wfPath, 'utf-8')) as WorkflowJSON; - const header = buildJsdocHeader(entry); - const code = emitInstanceAi(wf, { jsdocHeader: header }); - files.push({ filename: `${entry.slug}.ts`, content: code }); - indexLines.push(buildIndexLine(entry)); - } - - const indexTxt = indexLines.join('\n') + (indexLines.length > 0 ? '\n' : ''); - return { files, indexTxt }; -} - -function buildJsdocHeader(entry: ManifestEntry): string { - // Description is intentionally omitted: it's untrusted author-supplied prose - // from the public catalog that the builder agent would read verbatim. Name + - // nodes + tags + source already disambiguate templates. - return [ - '/**', - ' * @template', - ` * @name ${entry.name}`, - ` * @nodes ${entry.nodes.join(', ')}`, - ` * @tags ${entry.tags.join(', ')}`, - ` * @source ${entry.source}`, - ` * @author ${entry.author}`, - ' */', - ].join('\n'); -} - -function buildIndexLine(entry: ManifestEntry): string { - const truncatedNodes = truncateNodes(entry.nodes); - return [ - `${entry.slug}.ts`, - entry.name, - truncatedNodes, - entry.tags.join(','), - `n8n:${entry.id}`, - ].join(' | '); -} - -function truncateNodes(nodes: string[]): string { - if (nodes.length <= NODES_INLINE_LIMIT) return nodes.join(INDEX_NODE_SEPARATOR); - const head = nodes.slice(0, NODES_INLINE_LIMIT).join(INDEX_NODE_SEPARATOR); - const remaining = nodes.length - NODES_INLINE_LIMIT; - return `${head} +${remaining} more`; -} diff --git a/packages/@n8n/workflow-sdk/src/examples-zip.ts b/packages/@n8n/workflow-sdk/src/examples-zip.ts deleted file mode 100644 index 13450dadd4c..00000000000 --- a/packages/@n8n/workflow-sdk/src/examples-zip.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Zip extraction utility for the curated workflow examples. - * - * The 106 workflow JSON files are committed as a single `examples/templates.zip` - * to keep the package small. At runtime the loader calls `ensureExtracted()` - * which extracts them on first use. The committed `manifest.json` is the - * source of truth and is NOT in the zip. - */ -import type TAdmZip from 'adm-zip'; -let _admZip: typeof TAdmZip | undefined; -function loadAdmZip(): typeof TAdmZip { - if (!_admZip) { - // adm-zip's CJS export is the constructor itself. - // eslint-disable-next-line @typescript-eslint/no-require-imports - const mod = require('adm-zip') as typeof TAdmZip; - _admZip = mod; - } - return _admZip; -} -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; - -const EXAMPLES_DIR = path.resolve(__dirname, '..', 'examples'); -const ZIP_PATH = path.join(EXAMPLES_DIR, 'templates.zip'); -const MANIFEST_PATH = path.join(EXAMPLES_DIR, 'manifest.json'); - -function sdkVersion(): string { - try { - const pkgPath = path.resolve(__dirname, '..', 'package.json'); - // eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse -- Own package.json - return ( - (JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version?: string }).version ?? - 'unversioned' - ); - } catch { - return 'unversioned'; - } -} - -// Tmp cache for unzipped workflows — keyed by SDK version so upgrades extract -// fresh. We can't unzip back into the package because node_modules is -// read-only inside n8n's Docker image. -export const WORKFLOWS_CACHE_DIR = path.join( - os.tmpdir(), - 'n8n-workflow-sdk', - sdkVersion(), - 'workflows', -); - -interface ManifestEntry { - slug: string; - success: boolean; - skip?: boolean; -} - -interface ManifestFile { - workflows: ManifestEntry[]; -} - -export function zipExists(): boolean { - return fs.existsSync(ZIP_PATH); -} - -/** - * True if the zip exists and at least one workflow file expected by the - * manifest is missing on disk. - */ -export function needsExtraction(): boolean { - if (!fs.existsSync(ZIP_PATH)) return false; - if (!fs.existsSync(MANIFEST_PATH)) return false; - - // eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse -- Internal manifest file - const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf-8')) as ManifestFile; - for (const entry of manifest.workflows ?? []) { - if (!entry.success || entry.skip) continue; - const filePath = path.join(WORKFLOWS_CACHE_DIR, `${entry.slug}.json`); - if (!fs.existsSync(filePath)) return true; - } - return false; -} - -/** - * Extract all workflow JSONs from the zip into `examples/workflows/`. - * The committed manifest.json is the source of truth and is not in the zip. - */ -export function extractFromZip(): void { - if (!fs.existsSync(ZIP_PATH)) { - throw new Error(`Examples zip not found: ${ZIP_PATH}`); - } - if (!fs.existsSync(WORKFLOWS_CACHE_DIR)) { - fs.mkdirSync(WORKFLOWS_CACHE_DIR, { recursive: true }); - } - - const AdmZip = loadAdmZip(); - const zip = new AdmZip(ZIP_PATH); - for (const entry of zip.getEntries()) { - if (entry.isDirectory) continue; - zip.extractEntryTo(entry, WORKFLOWS_CACHE_DIR, false, true); - } -} - -export function ensureExtracted(): void { - if (needsExtraction()) { - extractFromZip(); - } -} diff --git a/packages/cli/src/modules/instance-ai/__tests__/instance-ai.adapter.service.security.test.ts b/packages/cli/src/modules/instance-ai/__tests__/instance-ai.adapter.service.security.test.ts index db542968e98..458ebee33a9 100644 --- a/packages/cli/src/modules/instance-ai/__tests__/instance-ai.adapter.service.security.test.ts +++ b/packages/cli/src/modules/instance-ai/__tests__/instance-ai.adapter.service.security.test.ts @@ -7,6 +7,15 @@ jest.mock('@n8n/instance-ai', () => ({ const safeContent = content.replace(/<\/untrusted_data/gi, '</untrusted_data'); return `\n${safeContent}\n`; }, + builderTemplatesOptionsFromEnv: () => ({}), + BuilderTemplatesService: class { + async getBundle() { + return { files: [], indexTxt: '', version: null }; + } + getVersion() { + return null; + } + }, })); import { mock } from 'jest-mock-extended'; @@ -111,7 +120,7 @@ const service = new InstanceAiAdapterService( workflowRunner, loadNodesAndCredentials, nodeTypes, - mock({ staticCacheDir: '/tmp/test-cache' }), + mock({ staticCacheDir: '/tmp/test-cache', n8nFolder: '/tmp/test-cache' }), dataTableService, dataTableRepository, dynamicNodeParametersService, diff --git a/packages/cli/src/modules/instance-ai/__tests__/instance-ai.adapter.service.test.ts b/packages/cli/src/modules/instance-ai/__tests__/instance-ai.adapter.service.test.ts index b1d80ef12b2..322d00420a7 100644 --- a/packages/cli/src/modules/instance-ai/__tests__/instance-ai.adapter.service.test.ts +++ b/packages/cli/src/modules/instance-ai/__tests__/instance-ai.adapter.service.test.ts @@ -7,6 +7,15 @@ jest.mock('@n8n/instance-ai', () => ({ const safeContent = content.replace(/<\/untrusted_data/gi, '</untrusted_data'); return `\n${safeContent}\n`; }, + builderTemplatesOptionsFromEnv: () => ({}), + BuilderTemplatesService: class { + async getBundle() { + return { files: [], indexTxt: '', version: null }; + } + getVersion() { + return null; + } + }, })); import type { ExecutionError, IRunExecutionData, ITaskData } from 'n8n-workflow'; @@ -973,7 +982,7 @@ function createNodeAdapterForTests(nodes: Array>) { {} as unknown as ConstructorParameters[11], {} as unknown as ConstructorParameters[12], {} as unknown as ConstructorParameters[13], - { staticCacheDir: '/tmp' } as unknown as ConstructorParameters< + { staticCacheDir: '/tmp', n8nFolder: '/tmp' } as unknown as ConstructorParameters< typeof InstanceAiAdapterService >[14], {} as unknown as ConstructorParameters[15], @@ -1110,7 +1119,7 @@ function createDataTableAdapterForTests(overrides?: { collectTypes: jest.fn().mockResolvedValue({ nodes: [], credentials: [] }), } as unknown as ConstructorParameters[12], {} as unknown as ConstructorParameters[13], - {} as unknown as ConstructorParameters[14], + { n8nFolder: '/tmp' } as unknown as ConstructorParameters[14], mockDataTableService as unknown as DataTableService, mockDataTableRepository as unknown as DataTableRepository, {} as unknown as ConstructorParameters[17], @@ -1392,7 +1401,7 @@ function createWorkflowAdapterForTests(overrides?: { collectTypes: jest.fn().mockResolvedValue({ nodes: [], credentials: [] }), } as unknown as ConstructorParameters[12], {} as unknown as ConstructorParameters[13], - {} as unknown as ConstructorParameters[14], + { n8nFolder: '/tmp' } as unknown as ConstructorParameters[14], {} as unknown as ConstructorParameters[15], {} as unknown as ConstructorParameters[16], {} as unknown as ConstructorParameters[17], @@ -1948,7 +1957,7 @@ function createExecutionAdapterForTests(overrides?: { sharingEnabled?: boolean } collectTypes: jest.fn().mockResolvedValue({ nodes: [], credentials: [] }), } as unknown as ConstructorParameters[12], {} as unknown as ConstructorParameters[13], - {} as unknown as ConstructorParameters[14], + { n8nFolder: '/tmp' } as unknown as ConstructorParameters[14], {} as unknown as ConstructorParameters[15], {} as unknown as ConstructorParameters[16], {} as unknown as ConstructorParameters[17], @@ -2214,7 +2223,7 @@ function createRunAdapterForTests( mockWorkflowRunner as unknown as ConstructorParameters[11], {} as unknown as ConstructorParameters[12], {} as unknown as ConstructorParameters[13], - {} as unknown as ConstructorParameters[14], + { n8nFolder: '/tmp' } as unknown as ConstructorParameters[14], {} as unknown as ConstructorParameters[15], {} as unknown as ConstructorParameters[16], {} as unknown as ConstructorParameters[17], diff --git a/packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts b/packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts index 88777dcf6e1..1ab14c515e8 100644 --- a/packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts +++ b/packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts @@ -35,7 +35,11 @@ import type { ServiceProxyConfig, CredentialTypeSearchResult, } from '@n8n/instance-ai'; -import { wrapUntrustedData } from '@n8n/instance-ai'; +import { + BuilderTemplatesService, + builderTemplatesOptionsFromEnv, + wrapUntrustedData, +} from '@n8n/instance-ai'; import type { WorkflowJSON } from '@n8n/workflow-sdk'; import { GlobalConfig } from '@n8n/config'; import { Time } from '@n8n/constants'; @@ -179,6 +183,8 @@ export class InstanceAiAdapterService { private readonly NODES_CACHE_TTL_MS = 5 * 60 * 1000; + private templatesService: BuilderTemplatesService | undefined; + private async getNodesFromCache(): Promise { if (this.nodesCache && Date.now() < this.nodesCache.expiresAt) { return await this.nodesCache.promise; @@ -250,6 +256,7 @@ export class InstanceAiAdapterService { dataTableService: this.createDataTableAdapter(user), webResearchService: this.createWebResearchAdapter(user, searchProxyConfig), workspaceService: this.createWorkspaceAdapter(user), + templatesService: this.getTemplatesService(), licenseHints: this.buildLicenseHints(), logger: this.logger, nodeTypesProvider: this.nodeTypes, @@ -257,6 +264,17 @@ export class InstanceAiAdapterService { }; } + private getTemplatesService(): BuilderTemplatesService { + if (!this.templatesService) { + this.templatesService = new BuilderTemplatesService({ + ...builderTemplatesOptionsFromEnv({ logger: this.logger }), + cacheDir: path.join(this.instanceSettings.n8nFolder, 'n8n-sdk-templates'), + logger: this.logger, + }); + } + return this.templatesService; + } + private buildLicenseHints(): string[] { const hints: string[] = []; if (!this.license.isLicensed('feat:namedVersions')) {