/** All valid node type identifiers */ export type NodeType = 'webhook' | 'scheduledWorkflow' | 'chatTrigger' | 'trigger' | 'voiceAgent' | 'filter' | 'transform' | 'schemaValidator' | 'conditional' | 'delay' | 'rateLimiter' | 'aggregator' | 'cache' | 'code' | 'ai' | 'merge' | 'approval' | 'split' | 'loop' | 'markdown' | 'fileTransform' | 'limit' | 'router' | 'emailAction' | 'gmailAction' | 'httpAction' | 'mongoAction' | 'postgresAction' | 'notificationAction' | 'sheetsAction' | 'calendarAction' | 'docsAction' | 'driveAction' | 'firecrawlAction' | 'telegramAction' | 'whatsappAction' | 'discordAction' | 'slackAction' | 'googleContactsAction' | 'googleAnalyticsAction' | 'notionAction' | 'vectorStore' | 'rssAction' | 'socialMediaAction' | 'mailchimpAction' | 'shopifyAction' | 'githubAction' | 'jiraAction' | 'bucketAction' | 'apifyAction' | 'calendlyAction' | 'airtableAction' | 'elevenlabsAction' | 'woocommerceAction' | 'outlookAction' | 'outlookCalendarAction' | 'onedriveAction' | 'excelAction' | 'wordAction' | 'teamsAction' | 'stripeAction' | 'hubspotAction' | 'todoistAction' | 'microsoftTodoAction' | 'sharepointAction' | 'stickyNote'; /** Node role in the pipeline */ export type NodeRole = 'source' | 'processing' | 'flowControl' | 'routing' | 'action' | 'monitoring'; /** Handle positions on the canvas node */ export type HandlePosition = 'left' | 'right' | 'top' | 'bottom'; /** Connection rules — which node types can connect to/from this node */ export interface NodeConnectionConfig { acceptsInputFrom: NodeType[]; canOutputTo: NodeType[]; special?: { hasRejectionOutputNodes?: boolean; hasBranches?: boolean; isTerminal?: boolean; hasLoopBack?: boolean; }; } /** UI-specific config for the dashboard canvas */ export interface NodeUIConfig { fromNodes: boolean; toNodes: boolean; isSource?: boolean; inputHandles?: HandlePosition[]; outputHandles?: HandlePosition[]; dotHandles?: { input?: string[]; output?: string[]; }; /** * When true, the canvas derives output handles from the node entity * at render time (e.g. one handle per `entity.tools[i].name`) rather * than from a static `dotHandles.output` list. Used by Voice Agent — * the count of tools is data, not config. NodeShell branches on this * flag to render the dynamic handle set. */ dynamicOutputs?: boolean; special?: { hasBranches?: boolean; isTerminal?: boolean; hasLoopBack?: boolean; /** * Aquí había también un `'rule'`, sólo para el router, y con él vivía en * el lienzo un camino de conexión a medida: su propio modal, su propio * `handleRuleSelect` y su propia puerta en `onConnect`. Dos mecanismos * para lo mismo, y así es como uno se arregla y el otro no: el modal del * router salía al ARRASTRAR y nunca al pulsar el `+`, porque ese segundo * camino pasa por `outputGroups` — que el router no declaraba. * * El router ramifica igual que el conditional, el split y el clasificador, * y su destino vive en el mismo sitio que el de ellos (`port: rule:`), * así que usa la misma maquinaria. Se quita del tipo para que no pueda * volver a declararse sin que el compilador lo diga. */ connectModal?: 'branch' | 'output-group'; routerTargetLabel?: string; /** * Los juegos de salidas de un nodo que ramifica: UNO, o VARIOS con su * `gate` cada uno [2026-09-22]. Con varios, quien pregunta se queda con * el PRIMERO cuya puerta pasa — ver `outputGroupsFor`. El AI Node los * necesita porque ramifica de dos maneras según el modo: por las * categorías que escribe el usuario (`classifier`) o por los cuatro * puertos fijos del análisis de sentimiento (`sentiment`). La forma * vieja (un objeto suelto) sigue valiendo y significa «sin alternativas». */ outputGroups?: OutputGroupsConfig | OutputGroupsConfig[]; /** * Maps output fields to alternate payload fields on the source entity. * When a downstream node is connected via a specific output field, * InputSection reads the alternate payload instead of lastPayload. * Example: { doneOutputNodes: 'lastDonePayload' } for loop nodes. */ alternatePayloads?: Record; /** Whether this node can be connected as a loop-back target. Only nodes that go through the unified node-lifecycle support loop-back detection. */ loopBackAllowed?: boolean; }; } /** * How the event pipeline routes a payload to this node when an * upstream webhook connects to it directly. Required so adding a * new node type at compile time is impossible without picking one. * * - 'generic': dispatchConnectedNodes loops findActiveByInputEndpoint * on the service and dispatches every active match. Use for * pipeline-style nodes that ingest webhook payloads (filter, * transform, ai, code, markdown, fileTransform, vectorStore). * * - 'custom': has a dedicated `case` in the dispatchOutputNodes * switch that calls the node's specific service method. Used * when the node needs scheduling, branching, gating, or queueing * (delay, cache, aggregator, rateLimiter, conditional, split, * loop, merge, approval, schemaValidator, router). The generic * loop must NOT process these — would dispatch twice. * * - 'excluded': not dispatched in the webhook flow at all. Action * nodes (email/http/mongo/notification/sheets/calendar/docs/ * firecrawl) are fired post-delivery in onDeliveryResult with * their own loops; ingress nodes (webhook/scheduledWorkflow/ * chatTrigger/trigger) start the flow themselves; * stickyNote is purely visual. */ export type PipelineDispatchKind = 'generic' | 'custom' | 'excluded'; /** API dispatch config — how the event pipeline processes each node */ export interface NodeDispatchConfig { service: string; collection: string; hasFilters: boolean; outputFields: string[]; /** @deprecated Use `pipelineDispatch` instead. Kept for backwards compat * during the migration; will be removed once every consumer reads * `pipelineDispatch`. */ customDispatch: boolean; /** Required. See PipelineDispatchKind for the contract per value. */ pipelineDispatch: PipelineDispatchKind; /** Required. See DownstreamPayload. */ downstreamPayload: DownstreamPayload; } /** * Que pone este nodo en la cinta para el de abajo. * * `'result'` es lo normal: baja lo que este nodo produjo. * * `'original'` es el nodo que NO transforma —notifica, o rutea—: baja su * ENTRADA intacta, y su propia salida (el acuse del envio, la rama que caso) * NO baja con ella. Nueve tipos hoy. * * ## Por que vive aqui y no solo en el handler * * Porque no es un detalle del ejecutor: decide QUE CAMPOS existen aguas abajo, * y de eso vive tambien el panel. Estaba escrito solo en los handlers de * hw-nodes, el dashboard no podia leerlo, y por eso la columna «Input» de un * nodo colgado de un Discord ensenaba el acuse del mensaje —`messageId`, * `channelId`, `content`— cuando lo que de verdad le llega es lo que entro al * Discord. Quien leyo esa columna escribio su plantilla contra campos que en * produccion no existen. * * El nombre y los dos valores son los MISMOS que declara el handler * (`NodeHandler.downstreamPayload` del SDK) a proposito: asi la comprobacion de * que no derivan es una igualdad y no una traduccion. Vive en hw-nodes, que es * donde estan los handlers. */ export type DownstreamPayload = 'original' | 'result'; /** Standardized payload metadata — every node output MUST include _meta */ export interface PayloadMeta { iterable: boolean; count: number; iterateField?: string; } /** Helper to create _meta for single-object outputs */ export declare function singleMeta(): PayloadMeta; /** Helper to create _meta for iterable array outputs */ export declare function iterableMeta(field: string, count: number): PayloadMeta; /** Canvas prefix → node type mapping */ export interface PrefixMapping { prefix: string; type: NodeType; /** Canvas node type used in React Flow nodeTypes map (e.g. "cache-node", "email-action") */ canvasType: string; } /** * Un juego de salidas de un nodo que ramifica. * * Vivía dentro de `NodeUIConfig.special` como un objeto anónimo; sale aquí * [2026-09-22] para poder declarar varios por nodo y para que quien los lea * nombre el tipo en vez de repetir su forma. */ export interface OutputGroupsConfig { /** Entity field containing the array of groups (e.g. 'outputs', 'branches') */ field?: string; /** Field within each group for display name (e.g. 'name') */ nameField?: string; /** Field within each group for summary/description (e.g. 'field', 'filterMode') */ summaryField?: string; /** * Varios campos del grupo, unidos por espacios, cuando el resumen que * de verdad identifica al grupo no cabe en uno solo. * * Existe por las reglas del router: su condición son TRES campos * (`field`, `operator`, `value`) y el modal a medida que se retiró las * pintaba juntas — «status eq active». Con un único `summaryField` una * regla sin `label` quedaba en «rule 1» y «status», que no dice a qué * se está conectando uno. * * Los campos vacíos se saltan, para que una regla con operador * `exists` no salga con un espacio colgando al final. */ summaryFields?: string[]; /** Label shown in the modal (e.g. 'output', 'branch') */ label: string; /** Entity field for the fallback/else path (e.g. 'elseOutputNodes') */ elseField?: string; /** * Fixed output groups — for nodes with a static set of outputs (e.g. approval: approve/reject). * When set, `field`/`nameField` are ignored — modal shows these fixed options. * * Cada entrada nombra su puerto de una de dos formas, y sólo una: * · `field` — un campo de primer nivel de los de ANTES de que los puertos * existieran (`rejectionOutputNodes` → puerto `rejected`); se traduce * con el mapa de campos heredados. * · `port` — el puerto, tal cual. Es lo que usan las salidas nacidas ya * con puertos (las cuatro de sentiment): inventarles un campo heredado * que nunca existió sería mentir en el registro. */ fixedGroups?: Array<{ name: string; field?: string; port?: string; /** Una línea de qué entrega ese puerto, para el modal de conexión. */ summary?: string; }>; /** * Optional content-aware gate — the output-group modal only opens * when `entity[gate.field] === gate.value`. Lets a node opt into * multi-branch routing only in some modes (e.g. AI Node in * classifier mode) without trapping every connection in the * picker the rest of the time. When the gate fails, the * FlowCanvas falls through to the universal * `entity.outputNodes` connect path. */ gate?: { field: string; value: unknown; }; /** * Qué es el puerto `main` de este nodo MIENTRAS su contenedor está * vacío. Sin esto, conectar a un nodo que ramifica y todavía no tiene * ninguna salida escribía una arista en `main` **en silencio**, y el * usuario no tenía forma de saber qué había pasado con ella. * * No hay una respuesta única, y por eso es un dato y no una regla. * Medido en `api/src/common/pipeline-run.service.ts`: * * 'passthrough' — router sin reglas: `rules.length === 0 * ? refsForPort(entity, MAIN_PORT) : []`. La * arista SÍ dispara... hasta que se cree la * primera regla, y entonces deja de hacerlo * sin avisar. * 'never-dispatched' — split: no tiene salida `main`, y el propio * despachador registra un warning por cada * arista que se quedó en ese puerto. * * Se declara sólo donde se ha medido. Sin declarar, el lienzo se * comporta como siempre: conecta por el camino universal y no dice * nada, que es lo que hacía antes de existir este campo. */ emptyContainerMain?: 'passthrough' | 'never-dispatched'; } /** * El juego de salidas que aplica AHORA a esta entidad, o `undefined` si el * nodo no ramifica en este momento. * * Es la única lectura de `special.outputGroups` que entiende las dos formas * (uno o varios) y las dos puertas (con `gate` o sin ella). Todo el que * pregunte tiene que pasar por aquí: el lienzo, el modal de conexión y el * despachador. Escribirlo cada uno a su manera es como el router acabó con un * camino propio que nadie arregló al arreglar el de los demás. */ export declare function outputGroupsFor(config: NodeUIConfig | undefined | null, entity: Record | undefined | null): OutputGroupsConfig | undefined; /** Todos los juegos declarados, pasen o no su puerta. Para quien necesita el * UNIVERSO de puertos que un tipo puede emitir (la auditoría de puertos), * no el de ahora mismo. */ export declare function todosLosOutputGroups(config: NodeUIConfig | undefined | null): OutputGroupsConfig[];