npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@linksolucoes/nier

v1.0.0

Published

Engine de automações baseada em grafos

Readme

@linksolucoes/nier

Engine de automações baseada em grafos (DAG) para orquestrar fluxos com nós de ação, decisão, paralelismo e espera. Focada em tipagem forte, validação configurável e extensibilidade via registries (actions, comparators, operand resolvers).

Índice

  • Conceitos rápidos
  • Instalação
  • Exemplo mínimo (end-to-end)
  • Tipos principais (modelo de domínio)
  • Validação (validateAutomation)
  • Engine (execução, eventos e scheduler)
    • Retries/backoff e histórico
  • Registry (extensão: actions, comparators, operands)
  • Operands e resolução de valores
  • Índices derivados de grafo
  • Exemplos adicionais
  • Schema compiler (JSON Schema + Manifest)
  • Roadmap e limitações
  • Contribuição e licença
  • Triggers (conceito e helper)

Conceitos rápidos

  • Automation: conjunto com grafo e triggers; fluxo começa em rootNodeId.
  • Graph: nós dirigidos (DAG) de tipos: action, decision, parallel, wait, end.
  • Decision: avalia condições (comparators/operands) e segue a primeira branch verdadeira; se nenhuma, usa defaultTo.
  • Parallel: abre múltiplos ramos e depois faz “join” (waitAll | waitAny | count) antes de seguir para to.
  • Wait: aguarda duração ou timestamp antes de seguir.
  • Registry: catálogo extensível com comparators, actions, resolvers, e regras de validação adicionais.

Princípios: tipos primeiro; funções puras onde possível; zero dependências externas no core; validação “fail fast”; extensibilidade controlada.

Instalação

Pacote:

pnpm add @linksolucoes/nier
# ou
npm i @linksolucoes/nier
# ou
yarn add @linksolucoes/nier

Requisitos sugeridos: Node 18+, TypeScript 5.5+. O core não possui dependências de runtime.

Exemplo mínimo (end-to-end)

Fluxo: action → decision (usa comparators) → wait/parallel → end. Executa com um scheduler inline.

import {
	coreRegistry,
	createComparator,
	createEngine,
	createRegistry,
	InlineSchedulerAdapter,
	InMemoryExecutionStore,
	mergeRegistry,
	type Automation,
} from "@linksolucoes/nier";

// 1) Comparators concretos
const eq = createComparator({
	id: "EQ",
	arity: 2,
	eval: async ([a, b]) => a === b,
});
const lt = createComparator({
	id: "LT",
	arity: 2,
	eval: async ([a, b]) => Number(a) < Number(b),
});
const registry = mergeRegistry(
	coreRegistry,
	createRegistry({ comparators: [eq, lt] })
);

// 2) Action kind simples
registry.actionKinds["log"] = {
	kind: "log",
	execute: async (params) => {
		console.log("[log]", params);
		return { status: "ok", data: params };
	},
};

// 3) Definição do fluxo
const automation: Automation = {
	meta: { id: "auto_1", name: "Exemplo" },
	rootNodeId: "start",
	graph: {
		nodes: [
			{
				id: "start",
				type: "action",
				action: { kind: "log", params: { msg: "start" } },
			},
			{
				id: "check",
				type: "decision",
				branches: [
					{
						id: "low",
						to: "waitShort",
						condition: {
							root: {
								type: "condition",
								comparator: "LT",
								left: { kind: "var", path: "user.score" },
								right: { kind: "const", value: 10 },
							},
						},
					},
				],
				defaultTo: "parallel",
			},
			{
				id: "waitShort",
				type: "wait",
				wait: { kind: "duration", durationMs: 50 },
				to: "end",
			},
			{
				id: "parallel",
				type: "parallel",
				branches: [
					{ id: "b1", start: "p1" },
					{ id: "b2", start: "p2" },
				],
				join: { strategy: "waitAll" },
				to: "end",
			},
			{
				id: "p1",
				type: "action",
				action: { kind: "log", params: { branch: 1 } },
			},
			{
				id: "p2",
				type: "action",
				action: { kind: "log", params: { branch: 2 } },
			},
			{ id: "end", type: "end" },
		],
		edges: [{ id: "e1", from: "start", to: "check" }],
	},
	triggers: [],
};

// 4) Engine + scheduler inline
const store = new InMemoryExecutionStore();
const engine = createEngine({
	runtime: {
		registry,
		store,
		onEvent: (e) => console.log("[event]", e),
	},
	scheduler: new InlineSchedulerAdapter({
		onFlowJob: async () => {},
		onNodeJob: async ({ executionId, nodeId }) => {
			await engine.handleNodeJob({
				automation,
				executionId,
				nodeId,
				userData: { score: 5 },
			});
		},
	}),
});

await engine.startFlowPerNode({ automation, executionId: "exec_1" });

Dica: um exemplo equivalente está em src/examples/basic.ts.

API pública (exports)

Disponível via @linksolucoes/nier (barrel src/index.ts):

  • Core: buildIndices, validateAutomation, listDefaultValidationRules, tipos (Automation, Graph, ConditionNode, Operand, ...)
  • Engine: createEngine, InlineSchedulerAdapter, InMemoryExecutionStore, tipos (EngineEvent, EngineSchedulerAdapter, ExecutionStore, ExecutionState)
  • Registry: coreRegistry, createRegistry, mergeRegistry, helpers (createComparator, createActionKind, createNodeKind, createOperandResolver, withValidationRules)
  • Schema: compileAutomationSchema, compileAutomationBundle
  • Triggers: createTriggerHelper

Tipos principais (modelo de domínio)

  • Nodes (type):
    • action: { id, type: 'action', action: { kind, params? } }
    • decision: { id, type: 'decision', branches: Array<{ id, to, condition? }>, defaultTo? }
    • parallel: { id, type: 'parallel', branches: Array<{ id, start }>, join?: { strategy: 'waitAll'|'waitAny'|'count', count? }, to? }
    • wait: { id, type: 'wait', wait: { kind: 'duration'|'until', durationMs?, untilTimestamp? }, to? }
    • end: { id, type: 'end' }
  • Edges opcionais: { id, from, to, condition? } (action → … via edges explícitas).
  • Conditions:
    • ConditionNode: group com op: AND|OR e children, ou condition com comparator, left e right?.
    • Operands: const | var | context | fn.
  • Automation: { meta, rootNodeId, graph, triggers }.

Tudo isso está tipado e exportado de src/core/types.ts (barrel em src/index.ts).

Validação (validateAutomation)

Função: validateAutomation({ automation, registry, indices?, options?, extraRules? }){ valid, issues, summary }.

  • Regras inclusas (base):
    • Básicas: root presente e existente, IDs únicos para nodes/edges/triggers, endpoints de edges válidos.
    • Estruturais: alcançabilidade (via root), detecção de ciclos, requisitos de decision/parallel/wait, throttle de triggers.
    • Conditions: comparators conhecidos, aviso de possível multi-match em decision sem default.
  • Extensão: withValidationRules(registry, rules) agrega regras custom ao registry para serem aplicadas.
  • Utils: listDefaultValidationRules() retorna os nomes de regras padrão.

Exemplo:

import { validateAutomation, coreRegistry } from "@linksolucoes/nier";
const result = validateAutomation({ automation, registry: coreRegistry });
if (!result.valid) {
	console.error(result.issues);
}

Issue shape: { level: 'error'|'warning'|'info', code, message, context? }.

Engine (execução, eventos e scheduler)

Criação: createEngine({ runtime, scheduler }).

  • runtime:
    • registry: Registry em uso (actions/comparators etc.).
    • store?: Persistência de estado (default: nenhuma). Fornecemos InMemoryExecutionStore para dev/test.
    • onEvent?: Handler de eventos de execução (observabilidade leve).
  • scheduler (adapter):
    • scheduleNode({ executionId, nodeId, delayMs? })
    • scheduleFlow({ executionId, delayMs? })
    • Adapter pronto: InlineSchedulerAdapter({ onNodeJob, onFlowJob }) para execuções em memória.

Métodos:

  • startFlow({ automation, executionId }): agenda um job por fluxo (consumidor chama onFlowJob).
  • startFlowPerNode({ automation, executionId }): agenda o primeiro node como job.
  • handleNodeJob({ automation, executionId, nodeId, userData? }): processa um job de node (chamado pelo adapter).

Eventos emitidos (EngineEvent):

  • flowStarted, flowCompleted.
  • nodeScheduled, nodeCompleted { result? }, nodeErrored { error }.
  • decisionMultiMatch { matchedBranchIds } quando múltiplas branches seriam verdadeiras (o engine segue a primeira).
  • edgeMultiMatch { toNodeIds } quando múltiplas edges condicionais a partir de um action são verdadeiras (todas são agendadas).
  • nodeRetryScheduled { attempt, delayMs } quando um retry é agendado para uma action.

Semânticas relevantes demonstradas nos testes (Vitest):

  • Decision: escolhe a primeira branch com condição verdadeira; se nenhuma, usa defaultTo.
  • Parallel join:
    • waitAll: dispara to quando todos os ramos finalizam.
    • waitAny: dispara após o primeiro ramo finalizar.
    • count: dispara quando completed >= count.
  • Wait: duration reagenda o próximo node com delayMs; until calcula delayMs até o timestamp.

Retries/backoff (ações) e histórico

  • Configure por action em registry.actionKinds[kind].retry:
    • maxAttempts (padrão 1)
    • backoffMs (padrão 0)
  • Quando uma action retorna { status: 'error' }, o engine não segue as edges e agenda um retry até atingir maxAttempts (emite nodeRetryScheduled).
  • Quando a action lança exceção, o engine emite nodeErrored, persiste state.data.__lastError e também agenda retry se configurado.
  • Contador de tentativas: state.exec.attempts[nodeId].
  • Histórico opcional: habilite runtime.options.enableHistory = true para gravar nodeScheduled, nodeCompleted, nodeErrored, flowCompleted em state.history.

Exemplo completo: veja src/examples/retry-history.ts.

Registry (extensão)

Criação e composição:

  • createRegistry({ nodeKinds?, actionKinds?, comparators?, operandResolvers? })
  • mergeRegistry(base, extra, { override = true })
  • coreRegistry com node kinds básicos e comparators base (EQ, NEQ, GT, LT, EXISTS).

Factories de conveniência (validações leves):

  • createComparator({ id, arity, eval })
  • createActionKind({ kind, execute, ... })
  • createNodeKind({ kind, ... })
  • createOperandResolver({ kind, resolve })
  • withValidationRules(registry, [ruleA, ruleB])

Action executor: (params, ctx) => Promise<{ status: 'ok'|'error', data?, error? }>.

Operands e resolução de valores

Usados principalmente em conditions (decision/edges/triggers):

  • const: usa value literal.
  • var: busca por caminho com precedência: execuser.dataflow.
    • Prefixos explícitos suportados: exec., user., flow..
  • context: busca de nível superior por key em execuser.dataflow.
  • fn: resolve via registry.operandResolvers[fnId] (custom).

Helpers de runtime (internos): resolveOperand, resolveOperands.

Índices derivados de grafo

buildIndices(graph){ outgoing, incoming, inDegree, outDegree, nodeMap }.

  • Edges explícitas e conexões implícitas por tipo de node (decision/defaultTo, parallel/branches/to, wait/to).
  • Útil para validação e execução.

Exemplos adicionais

  • src/examples/basic.ts: fluxo com log, decision por LT, wait de 50ms, paralelismo com waitAll.
  • Testes em src/engine/__tests__: cobrem decisão (multi-match), estratégias de join, e wait + parallel.

Schema compiler (JSON Schema + Manifest)

Por que: gerar um JSON Schema do modelo de Automation para ótima DX (autocomplete/validadores no editor) e consumo por front-ends/LLMs. Também exporta um Manifest de capacidades derivado do seu registry (actions, comparators, operand resolvers, regras de validação).

APIs:

  • compileAutomationSchema(registry, options?){ schema, manifest }
  • compileAutomationBundle(automation, registry, options?){ schema, manifest, automation }

Options aceitas:

  • schemaId?: define $id no JSON Schema gerado.
  • draft?: versão do draft ("2020-12" padrão).

O que o Manifest contém (resumo):

  • nodeKinds: { kind, category?, description? }[]
  • actionKinds: { kind, displayName?, category?, schema? }[] (schema é repassado para validar action.params daquela kind)
  • comparators: { id, arity }[]
  • operandResolvers: { kind }[]
  • validationRuleNames: string[]

Como usar (exemplo rápido):

import {
	coreRegistry,
	createActionKind,
	createRegistry,
	mergeRegistry,
	compileAutomationSchema,
	compileAutomationBundle,
} from "@linksolucoes/nier";

const sendEmail = createActionKind({
	kind: "send_email",
	schema: {
		type: "object",
		required: ["templateId", "to"],
		properties: { templateId: { type: "string" }, to: { type: "string" } },
	},
});
const registry = mergeRegistry(
	coreRegistry,
	createRegistry({ actionKinds: [sendEmail] })
);

const { schema, manifest } = compileAutomationSchema(registry, {
	schemaId: "https://example.com/schemas/automation.json",
});

// Ou com a Automation embutida
const bundle = compileAutomationBundle(
	{
		/* ...Automation... */
	} as any,
	registry
);

Detalhes de validação no schema:

  • ActionNode.action.params é validado com base no schema do action kind correspondente (quando fornecido), via oneOf.
  • ConditionNode diferencia comparadores unários e binários (com/sem right).
  • Operand inclui kind: "fn" e também os operandResolvers do registry; quando kind == "fn" exige fnId válido.
  • ParallelNode.join e WaitNode.wait usam oneOf para exigir os campos apropriados por estratégia/kind.

Veja o exemplo completo em src/examples/schema-compiler.ts.

Triggers (conceito e helper)

Triggers são a forma de iniciar execuções quando eventos ocorrem no seu sistema. A arquitetura de escuta e despacho é responsabilidade do usuário (ex.: NestJS event listeners, filas, webhooks). Para melhorar a DX, a biblioteca oferece um helper para criar handlers fortemente tipados que avaliam filtros opcionais e retornam executionId para iniciar o fluxo.

API:

  • createTriggerHelper({ id, event, filter?, exec }){ event, handle(evt, runtime) }
    • filter?: ConditionNode avaliado com os comparators do registry.
    • exec: { makeExecutionId(evt), selectAutomation(evt), mapUserData?(evt), mode? }.
    • handle retorna { started: boolean, executionId? }.

Exemplo (pseudocódigo):

import { createTriggerHelper, createComparator, createRegistry, mergeRegistry, coreRegistry } from "@linksolucoes/nier";
import { createEngine, InlineSchedulerAdapter, InMemoryExecutionStore } from "@linksolucoes/nier";

// Comparators reais
const eq = createComparator({ id: "EQ", arity: 2, eval: async ([a, b]) => a === b });
const registry = mergeRegistry(coreRegistry, createRegistry({ comparators: [eq] }));

// Sua automation
const automation = /* ... */;

// Engine (per-node)
const store = new InMemoryExecutionStore();
const engine = createEngine({
	runtime: { registry, store, onEvent: (e) => console.log(e) },
	scheduler: new InlineSchedulerAdapter({
		onNodeJob: async ({ executionId, nodeId }) => engine.handleNodeJob({ automation, executionId, nodeId }),
		onFlowJob: async () => {},
	}),
});

// Helper
const userCreated = createTriggerHelper<{ kind: string; id: string }>({
	id: "t_user_created",
	event: "user.created",
	filter: { type: "condition", comparator: "EQ", left: { kind: "var", path: "user.kind" }, right: { kind: "const", value: "user.created" } },
	exec: {
		makeExecutionId: (e) => `exec_${e.id}`,
		selectAutomation: () => automation,
		mapUserData: (e) => ({ kind: e.kind, user: { id: e.id } }),
		mode: "per-node",
	},
});

// Listener no seu app
async function onUserEvent(evt: { kind: string; id: string }) {
	const res = await userCreated.handle(evt, engine.runtime);
	if (res.started) {
		// Inicie o fluxo de fato
		await engine.startFlowPerNode({ automation, executionId: res.executionId! });
	}
}

Checks externos nas condições (seu sistema)

Você pode consultar dados do seu sistema nas condições usando operand resolvers do tipo fn (assíncronos). Exemplo:

import {
	createOperandResolver,
	createRegistry,
	mergeRegistry,
	coreRegistry,
} from "@linksolucoes/nier";

// Resolver que calcula dias desde o cadastro via repositório/cliente externo
const daysSinceSignup = createOperandResolver({
	kind: "days_since_signup", // usado como fnId nas condições
	resolve: async (_operand, runtime) => {
		const userId = runtime.user.data.userId as string;
		const createdAt = await repo.getUserCreatedAt(userId);
		const ms = Date.now() - new Date(createdAt).getTime();
		return Math.floor(ms / 86_400_000);
	},
});

const reg = mergeRegistry(
	coreRegistry,
	createRegistry({ operandResolvers: [daysSinceSignup] })
);

// Condition: days_since_signup > 5
const filter = {
	type: "condition",
	comparator: "GT",
	left: { kind: "fn", fnId: "days_since_signup" },
	right: { kind: "const", value: 5 },
} as const;

Também é válido pré-computar e passar via mapUserData (ex.: daysSinceSignup) e então usar var com user.daysSinceSignup.

Validação (regras e códigos)

Regras padrão (src/validation/rules):

  • Básicas: root presente/existente; IDs únicos (nodes/edges/triggers); endpoints de edges.
  • Estruturais: alcançabilidade; ciclos; semântica de decision/parallel/wait; throttle de triggers.
  • Condições: comparators conhecidos; aviso de possível multi-match.

Códigos (IssueCodes):

  • GRAPH_ROOT_MISSING, GRAPH_ROOT_NOT_FOUND
  • NODE_DUPLICATE_ID, EDGE_DUPLICATE_ID, TRIGGER_DUPLICATE_ID
  • EDGE_NODE_MISSING, NODE_UNREACHABLE, CYCLE_DETECTED
  • DECISION_NO_BRANCHES, DECISION_NO_CONDITION, DECISION_POSSIBLE_MULTI_MATCH
  • PARALLEL_BRANCH_COUNT, PARALLEL_JOIN_COUNT_INVALID
  • WAIT_INVALID_CONFIG
  • THROTTLE_INVALID
  • COMPARATOR_UNKNOWN

Issue shape: { level: 'error'|'warning'|'info', code, message, context? }.