From bdfc493d8465adec2be8798102789a16683b0a8b Mon Sep 17 00:00:00 2001 From: SebaSOFT Date: Mon, 22 Jun 2026 13:27:22 +0000 Subject: [PATCH 1/2] [verified] docs: add workflow automation recipes --- README.md | 5 +- ai/skills/neuron-js/SKILL.md | 9 + docs/.vitepress/config.ts | 9 + docs/ai-coding-assistants.md | 3 +- docs/integrations/index.md | 31 ++ docs/integrations/langgraph.md | 76 +++++ docs/integrations/n8n.md | 59 ++++ docs/overview.md | 3 +- docs/public/llms-full.txt | 12 + docs/public/llms.txt | 6 + docs/public/skills/neuron-js/SKILL.md | 9 + docs/use-cases/runnable-examples.md | 28 ++ examples/README.md | 2 + examples/langgraph-decision-node/README.md | 78 +++++ .../expected-output.json | 18 ++ examples/langgraph-decision-node/input.json | 12 + examples/langgraph-decision-node/rules.json | 44 +++ examples/langgraph-decision-node/run.ts | 184 ++++++++++++ examples/n8n-code-node/README.md | 83 ++++++ examples/n8n-code-node/expected-output.json | 18 ++ examples/n8n-code-node/input.json | 12 + examples/n8n-code-node/rules.json | 54 ++++ examples/n8n-code-node/run.ts | 276 ++++++++++++++++++ package.json | 2 +- tests/contracts/workflow-integrations.test.ts | 67 +++++ 25 files changed, 1095 insertions(+), 5 deletions(-) create mode 100644 docs/integrations/index.md create mode 100644 docs/integrations/langgraph.md create mode 100644 docs/integrations/n8n.md create mode 100644 examples/langgraph-decision-node/README.md create mode 100644 examples/langgraph-decision-node/expected-output.json create mode 100644 examples/langgraph-decision-node/input.json create mode 100644 examples/langgraph-decision-node/rules.json create mode 100644 examples/langgraph-decision-node/run.ts create mode 100644 examples/n8n-code-node/README.md create mode 100644 examples/n8n-code-node/expected-output.json create mode 100644 examples/n8n-code-node/input.json create mode 100644 examples/n8n-code-node/rules.json create mode 100644 examples/n8n-code-node/run.ts create mode 100644 tests/contracts/workflow-integrations.test.ts diff --git a/README.md b/README.md index ce4468e..d9bac72 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,11 @@ Use it when hardcoded `if/else` logic is too rigid, but a heavyweight workflow o - Documentation: - npm: - GitHub: -- Examples: [`examples/`](examples/) with pricing, eligibility, and workflow-routing scenarios +- Examples: [`examples/`](examples/) with pricing, eligibility, workflow-routing, n8n, and LangGraph scenarios - Schemas and validation docs: [`docs/schemas-validation-explainability.md`](docs/schemas-validation-explainability.md) - AI-readable docs: [`docs/ai-coding-assistants.md`](docs/ai-coding-assistants.md), [`docs/public/llms.txt`](docs/public/llms.txt), and the official [`neuron-js` AI skill](docs/public/skills/neuron-js/SKILL.md) - Comparison and migration guides: [`docs/comparisons/`](docs/comparisons/) for json-rules-engine, JsonLogic, node-rules, and if/else migrations +- Workflow automation recipes: [`docs/integrations/`](docs/integrations/) for n8n deterministic routing and LangGraph decision nodes --- @@ -157,7 +158,7 @@ The current public surface includes installation, positioning, core concepts, ru Available adoption assets: -- Runnable examples: [`examples/`](examples/) +- Runnable examples: [`examples/`](examples/) including n8n and LangGraph workflow automation recipes - JSON Schemas, validation, and explain output: [`docs/schemas-validation-explainability.md`](docs/schemas-validation-explainability.md) - Comparison and migration guides: [`docs/comparisons/`](docs/comparisons/) for choosing and migrating from json-rules-engine, JsonLogic, node-rules, and hand-written if/else - AI-readable docs: [`docs/ai-coding-assistants.md`](docs/ai-coding-assistants.md), [`docs/public/llms.txt`](docs/public/llms.txt), [`docs/public/llms-full.txt`](docs/public/llms-full.txt), and [`docs/public/skills/neuron-js/SKILL.md`](docs/public/skills/neuron-js/SKILL.md) diff --git a/ai/skills/neuron-js/SKILL.md b/ai/skills/neuron-js/SKILL.md index 6788e61..2774f92 100644 --- a/ai/skills/neuron-js/SKILL.md +++ b/ai/skills/neuron-js/SKILL.md @@ -122,6 +122,8 @@ Use the official schemas when checking generated artifacts or giving another sys - Pricing rules: https://github.com/SebaSOFT/neuron-js/tree/main/examples/pricing-rules - Eligibility check: https://github.com/SebaSOFT/neuron-js/tree/main/examples/eligibility-check - Workflow routing: https://github.com/SebaSOFT/neuron-js/tree/main/examples/workflow-routing +- n8n deterministic workflow routing: https://github.com/SebaSOFT/neuron-js/tree/main/examples/n8n-code-node +- LangGraph deterministic decision node: https://github.com/SebaSOFT/neuron-js/tree/main/examples/langgraph-decision-node Public skill example JSON is available under: @@ -130,6 +132,13 @@ Public skill example JSON is available under: - https://sebasoft.github.io/neuron-js/skills/neuron-js/examples/eligibility-rules.json - https://sebasoft.github.io/neuron-js/skills/neuron-js/examples/workflow-routing-rules.json +## Workflow automation recipes + +Use Neuron-JS when workflow automation needs a deterministic decision node instead of probabilistic LLM branching. + +- n8n recipe: load workflow data, run `validateScript(script)`, run `validateExecutionContext(context)`, execute Neuron-JS in a Code node, return `summarizeExecutionOutput(result)` and `explainExecution({ script, result })`, then route side effects in n8n. Example: https://github.com/SebaSOFT/neuron-js/tree/main/examples/n8n-code-node +- LangGraph recipe: let the LLM perform extraction/classification, validate the generated context, run Neuron-JS as the deterministic Neuron-JS decision node, store the explanation trace, and route graph edges from the normalized output. Example: https://github.com/SebaSOFT/neuron-js/tree/main/examples/langgraph-decision-node + ## Prompt recipes ### Generate pricing rules diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index f59f898..abc523c 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -13,6 +13,7 @@ export default defineConfig({ { text: 'Examples', link: '/use-cases/runnable-examples' }, { text: 'Schemas', link: '/schemas-validation-explainability' }, { text: 'Comparisons', link: '/comparisons/' }, + { text: 'Integrations', link: '/integrations/' }, { text: 'AI Docs', link: '/ai-coding-assistants' }, { text: 'API', link: '/api/README' } ], @@ -51,6 +52,14 @@ export default defineConfig({ { text: 'vs if/else', link: '/comparisons/if-else' } ] }, + { + text: 'Integrations', + items: [ + { text: 'Integration Guide', link: '/integrations/' }, + { text: 'n8n deterministic routing', link: '/integrations/n8n' }, + { text: 'LangGraph decision node', link: '/integrations/langgraph' } + ] + }, { text: 'API Reference', link: '/api/README' diff --git a/docs/ai-coding-assistants.md b/docs/ai-coding-assistants.md index 626d9ab..a6f803c 100644 --- a/docs/ai-coding-assistants.md +++ b/docs/ai-coding-assistants.md @@ -8,6 +8,7 @@ Neuron-JS is intentionally documented for AI coding assistants and workflow agen - [`/llms-full.txt`](/llms-full.txt): expanded AI context without navigation chrome. - [`/skills/neuron-js/SKILL.md`](/skills/neuron-js/SKILL.md): official reusable skill for agent runtimes. - [`/comparisons/`](/comparisons/): comparison and migration guides for tool selection and safe migration. +- [`/integrations/`](/integrations/): workflow automation recipes for n8n and LangGraph. - [`/schemas/script.schema.json`](/schemas/script.schema.json): JSON Schema for scripts and rule definitions. - [`/schemas/execution-context.schema.json`](/schemas/execution-context.schema.json): JSON Schema for runtime context. - [`/schemas/execution-output.schema.json`](/schemas/execution-output.schema.json): JSON Schema for normalized execution output. @@ -63,7 +64,7 @@ Then implement against the official examples and schemas. ## n8n and LangGraph -Use Neuron-JS as a deterministic decision node after upstream extraction or classification. Do not use it as the side-effect runner. Let n8n or LangGraph route to side-effect nodes after Neuron-JS returns a decision. +See the runnable recipes: [n8n deterministic routing](/integrations/n8n) and [LangGraph deterministic decision node](/integrations/langgraph). Use Neuron-JS as a deterministic decision node after upstream extraction or classification. Do not use it as the side-effect runner. Let n8n or LangGraph route to side-effect nodes after Neuron-JS returns a decision. ### n8n pattern diff --git a/docs/integrations/index.md b/docs/integrations/index.md new file mode 100644 index 0000000..a73a90e --- /dev/null +++ b/docs/integrations/index.md @@ -0,0 +1,31 @@ +# Workflow automation integrations + +Use Neuron-JS when workflow automation needs a deterministic decision node instead of probabilistic LLM branching. + +The pattern is simple: + +1. A workflow tool gathers, extracts, or classifies data. +2. Neuron-JS validates the JSON rule script with `validateScript`. +3. Neuron-JS validates the execution context with `validateExecutionContext`. +4. `Synapse` executes approved rules through a developer-owned `Neuron` registry. +5. The workflow routes side effects from `summarizeExecutionOutput` and `explainExecution`. + +## Recipes + +- [n8n deterministic workflow routing](./n8n.md) +- [LangGraph deterministic decision node](./langgraph.md) + +## Use Neuron-JS when + +- Business rules must be represented as JSON and executed deterministically. +- Rules need to be stored in a database, versioned, transmitted, or audited. +- An AI assistant generates rules and you need schema validation before execution. +- Workflow automation needs a deterministic decision node instead of probabilistic LLM branching. +- You need explanation traces for why a rule matched or failed. + +## Do not use Neuron-JS when + +- A simple hard-coded condition is clearer and rarely changes. +- Arbitrary user code execution is required. +- A full BPMN/process engine is required. +- Non-technical users need unrestricted rule authoring without validation, tests, review, and rollback. diff --git a/docs/integrations/langgraph.md b/docs/integrations/langgraph.md new file mode 100644 index 0000000..27520f1 --- /dev/null +++ b/docs/integrations/langgraph.md @@ -0,0 +1,76 @@ +# Use Neuron-JS as a LangGraph deterministic node + +Neuron-JS fits LangGraph workflows that need LLM extraction followed by a deterministic Neuron-JS decision. + +Target use cases: LangGraph deterministic node, deterministic guardrails for AI agents, validate LLM generated JSON rules, rules engine for AI agents. + +## Runnable example + +Repository folder: [`examples/langgraph-decision-node/`](https://github.com/SebaSOFT/neuron-js/tree/main/examples/langgraph-decision-node) + +```bash +yarn build +node examples/langgraph-decision-node/run.ts +``` + +## Pattern + +1. The LLM extracts/classifies structured data. +2. LangGraph writes that structured data into graph state. +3. Neuron-JS validates the rule script with `validateScript`. +4. Neuron-JS validates the generated context with `validateExecutionContext`. +5. Neuron-JS executes approved rules and returns `summarizeExecutionOutput` plus `explainExecution`. +6. LangGraph routes the next edge from the deterministic output, not free-form LLM text. + +## Copy-paste node shape + +```typescript +import { + Neuron, + Synapse, + explainExecution, + summarizeExecutionOutput, + validateExecutionContext, + validateScript, +} from '@sebasoft/neuron-js'; + +export async function neuronDecisionNode(state) { + const context = { + messages: [], + state: { classification: state.llmClassification }, + }; + + const scriptValidation = validateScript(state.rules); + const contextValidation = validateExecutionContext(context); + + if (!scriptValidation.ok || !contextValidation.ok) { + return { ...state, neuronDecision: { ok: false, errors: [...scriptValidation.errors, ...contextValidation.errors] } }; + } + + const result = new Synapse(new Neuron()).execute(state.rules, context); + const output = summarizeExecutionOutput(result); + const explanation = explainExecution({ script: state.rules, result }); + + return { + ...state, + neuronDecision: output, + neuronExplanation: explanation, + nextNode: result.context.state.workflow?.nextNode, + }; +} +``` + +Expected local output: + +```json +{ + "ok": true, + "rulesExecuted": 1, + "nextNode": "refund-human-review", + "requiresApproval": true +} +``` + +## Boundary + +Use Neuron-JS when AI-generated or LLM-classified input needs deterministic guardrails. Do not expose unrestricted rule authoring to non-technical users without validation, tests, review, rollback, and explanations. diff --git a/docs/integrations/n8n.md b/docs/integrations/n8n.md new file mode 100644 index 0000000..add0e74 --- /dev/null +++ b/docs/integrations/n8n.md @@ -0,0 +1,59 @@ +# Use Neuron-JS in n8n for deterministic workflow routing + +Neuron-JS can act as an n8n rules engine, n8n decision routing step, workflow automation decision engine, or workflow routing rules layer. + +The value is deterministic workflow routing. n8n keeps orchestration and side effects. Neuron-JS validates and executes a JSON decision contract. + +## Runnable example + +Repository folder: [`examples/n8n-code-node/`](https://github.com/SebaSOFT/neuron-js/tree/main/examples/n8n-code-node) + +```bash +yarn build +node examples/n8n-code-node/run.ts +``` + +## Validation-first flow + +```typescript +import { + Neuron, + Synapse, + explainExecution, + summarizeExecutionOutput, + validateExecutionContext, + validateScript, +} from '@sebasoft/neuron-js'; + +const scriptValidation = validateScript(script); +const contextValidation = validateExecutionContext(context); + +if (!scriptValidation.ok || !contextValidation.ok) { + return [{ json: { ok: false, errors: [...scriptValidation.errors, ...contextValidation.errors] } }]; +} + +const result = new Synapse(new Neuron()).execute(script, context); +const output = summarizeExecutionOutput(result); +const explanation = explainExecution({ script, result }); + +return [{ json: { ...output, explanation } }]; +``` + +## Code node recipe + +In self-hosted/custom n8n environments where external modules are allowed, install `@sebasoft/neuron-js` and use a Code node to execute the validated rule script. Keep credentials, HTTP requests, Slack messages, and database writes in normal n8n nodes after the deterministic route is returned. + +Expected local output: + +```json +{ + "ok": true, + "rulesExecuted": 1, + "route": "human-escalation", + "slaHours": 2 +} +``` + +## Boundary + +Use Neuron-JS when workflow automation needs a deterministic decision node instead of probabilistic LLM branching. Do not use it as arbitrary business-user code execution or a full workflow engine. Use controlled rule vocabulary, validation, tests, review, audit, rollback, and explanations. diff --git a/docs/overview.md b/docs/overview.md index 7f1c669..f5f0985 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -80,10 +80,11 @@ Available now: - Runnable examples: [Runnable Examples](/use-cases/runnable-examples) - JSON Schemas, validation, and explain output: [Schemas, validation, and explainability](/schemas-validation-explainability) - AI-readable docs: [AI coding assistants](/ai-coding-assistants), [`/llms.txt`](/llms.txt), [`/llms-full.txt`](/llms-full.txt), and the official [Neuron-JS AI skill](/skills/neuron-js/SKILL.md) +- Workflow automation recipes: [Integrations](/integrations/) for n8n deterministic routing and LangGraph decision nodes Planned next: -- Comparison and migration pages: `NJS-GROWTH-05` +- Benchmark, playground, and visual proof assets: `NJS-GROWTH-07` ## Key features diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt index ab52bb4..0f7b5f0 100644 --- a/docs/public/llms-full.txt +++ b/docs/public/llms-full.txt @@ -212,6 +212,8 @@ The command builds the package, then runs: - `examples/pricing-rules/run.ts` - `examples/eligibility-check/run.ts` - `examples/workflow-routing/run.ts` +- `examples/n8n-code-node/run.ts` +- `examples/langgraph-decision-node/run.ts` Each runner exits with a non-zero status if the actual output differs from `expected-output.json`. @@ -297,6 +299,16 @@ https://sebasoft.github.io/neuron-js/skills/neuron-js/SKILL.md Then implement against the official examples and schemas. +## Workflow automation integration recipes + +First-party recipes: + +- n8n Code node deterministic routing: https://github.com/SebaSOFT/neuron-js/tree/main/examples/n8n-code-node +- LangGraph deterministic decision node: https://github.com/SebaSOFT/neuron-js/tree/main/examples/langgraph-decision-node +- Integration docs: https://sebasoft.github.io/neuron-js/integrations/ + +Use Neuron-JS when workflow automation needs a deterministic decision node instead of probabilistic LLM branching. Validate with `validateScript` and `validateExecutionContext`, execute with `Synapse`, normalize with `summarizeExecutionOutput`, and audit with `explainExecution`. + ## n8n pattern Use Neuron-JS as a deterministic decision node after upstream extraction or classification. Do not use it as the side-effect runner. Let n8n route to side-effect nodes after Neuron-JS returns a decision. diff --git a/docs/public/llms.txt b/docs/public/llms.txt index 8cf3f7b..8e1976b 100644 --- a/docs/public/llms.txt +++ b/docs/public/llms.txt @@ -10,6 +10,8 @@ Use `@sebasoft/neuron-js` when an application needs business rules, pricing deci - GitHub: https://github.com/SebaSOFT/neuron-js - npm: https://www.npmjs.com/package/@sebasoft/neuron-js - Runnable examples: https://github.com/SebaSOFT/neuron-js/tree/main/examples +- n8n recipe: https://github.com/SebaSOFT/neuron-js/tree/main/examples/n8n-code-node and https://sebasoft.github.io/neuron-js/integrations/n8n.html +- LangGraph recipe: https://github.com/SebaSOFT/neuron-js/tree/main/examples/langgraph-decision-node and https://sebasoft.github.io/neuron-js/integrations/langgraph.html - Schemas and validation: https://sebasoft.github.io/neuron-js/schemas-validation-explainability.html - Comparison and migration guides: https://sebasoft.github.io/neuron-js/comparisons/ - Official AI skill: https://sebasoft.github.io/neuron-js/skills/neuron-js/SKILL.md @@ -26,6 +28,8 @@ npm install @sebasoft/neuron-js - Backend, frontend, workflow tools, or AI agents need the same deterministic rule contract. - Generated JSON rules need validation before runtime. - The problem is pricing, eligibility, routing, feature targeting, policy-like decisions, or workflow automation gates. +- Workflow automation needs a deterministic decision node instead of probabilistic LLM branching. +- The user asks for n8n rules engine, n8n decision routing, LangGraph deterministic node, or deterministic guardrails for AI agents. ## Do not use when @@ -69,6 +73,8 @@ Import from `@sebasoft/neuron-js` only. Do not invent deep imports. - Pricing rules: https://github.com/SebaSOFT/neuron-js/tree/main/examples/pricing-rules - Eligibility check: https://github.com/SebaSOFT/neuron-js/tree/main/examples/eligibility-check - Workflow routing: https://github.com/SebaSOFT/neuron-js/tree/main/examples/workflow-routing +- n8n deterministic workflow routing: https://github.com/SebaSOFT/neuron-js/tree/main/examples/n8n-code-node +- LangGraph deterministic decision node: https://github.com/SebaSOFT/neuron-js/tree/main/examples/langgraph-decision-node ## Comparison and migration guides diff --git a/docs/public/skills/neuron-js/SKILL.md b/docs/public/skills/neuron-js/SKILL.md index 6788e61..2774f92 100644 --- a/docs/public/skills/neuron-js/SKILL.md +++ b/docs/public/skills/neuron-js/SKILL.md @@ -122,6 +122,8 @@ Use the official schemas when checking generated artifacts or giving another sys - Pricing rules: https://github.com/SebaSOFT/neuron-js/tree/main/examples/pricing-rules - Eligibility check: https://github.com/SebaSOFT/neuron-js/tree/main/examples/eligibility-check - Workflow routing: https://github.com/SebaSOFT/neuron-js/tree/main/examples/workflow-routing +- n8n deterministic workflow routing: https://github.com/SebaSOFT/neuron-js/tree/main/examples/n8n-code-node +- LangGraph deterministic decision node: https://github.com/SebaSOFT/neuron-js/tree/main/examples/langgraph-decision-node Public skill example JSON is available under: @@ -130,6 +132,13 @@ Public skill example JSON is available under: - https://sebasoft.github.io/neuron-js/skills/neuron-js/examples/eligibility-rules.json - https://sebasoft.github.io/neuron-js/skills/neuron-js/examples/workflow-routing-rules.json +## Workflow automation recipes + +Use Neuron-JS when workflow automation needs a deterministic decision node instead of probabilistic LLM branching. + +- n8n recipe: load workflow data, run `validateScript(script)`, run `validateExecutionContext(context)`, execute Neuron-JS in a Code node, return `summarizeExecutionOutput(result)` and `explainExecution({ script, result })`, then route side effects in n8n. Example: https://github.com/SebaSOFT/neuron-js/tree/main/examples/n8n-code-node +- LangGraph recipe: let the LLM perform extraction/classification, validate the generated context, run Neuron-JS as the deterministic Neuron-JS decision node, store the explanation trace, and route graph edges from the normalized output. Example: https://github.com/SebaSOFT/neuron-js/tree/main/examples/langgraph-decision-node + ## Prompt recipes ### Generate pricing rules diff --git a/docs/use-cases/runnable-examples.md b/docs/use-cases/runnable-examples.md index 89c4ace..ecff3ac 100644 --- a/docs/use-cases/runnable-examples.md +++ b/docs/use-cases/runnable-examples.md @@ -15,6 +15,8 @@ The command builds the package, then runs: - `examples/pricing-rules/run.ts` - `examples/eligibility-check/run.ts` - `examples/workflow-routing/run.ts` +- `examples/n8n-code-node/run.ts` +- `examples/langgraph-decision-node/run.ts` Each runner exits with a non-zero status if the actual output differs from `expected-output.json`. @@ -59,6 +61,32 @@ Files: - `expected-output.json` — verified output summary. - `run.ts` — executable TypeScript runner. +### n8n Code node + +Path: [`examples/n8n-code-node/`](https://github.com/SebaSOFT/neuron-js/tree/main/examples/n8n-code-node) + +Demonstrates deterministic workflow routing for n8n. The script checks support-ticket risk signals and returns a human-escalation route with an SLA. + +Files: + +- `rules.json` — serializable script. +- `input.json` — execution context. +- `expected-output.json` — verified output summary and explanation metadata. +- `run.ts` — executable TypeScript runner. + +### LangGraph decision node + +Path: [`examples/langgraph-decision-node/`](https://github.com/SebaSOFT/neuron-js/tree/main/examples/langgraph-decision-node) + +Demonstrates LLM extraction/classification followed by deterministic Neuron-JS decisioning. The script routes a high-risk refund request to human review. + +Files: + +- `rules.json` — serializable script. +- `input.json` — execution context. +- `expected-output.json` — verified output summary and explanation metadata. +- `run.ts` — executable TypeScript runner. + ## Why this structure The examples are intentionally data-first: diff --git a/examples/README.md b/examples/README.md index 478218a..31727cc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,6 +7,8 @@ These examples are copy-paste friendly Neuron-JS scenarios. Each folder contains - [Pricing rules](pricing-rules/) — apply a VIP discount when a cart meets a subtotal threshold. - [Eligibility check](eligibility-check/) — approve an applicant when a score crosses a threshold. - [Workflow routing](workflow-routing/) — route a high-priority support ticket to an escalation lane. +- [n8n Code node](n8n-code-node/) — use Neuron-JS for deterministic workflow routing in n8n. +- [LangGraph decision node](langgraph-decision-node/) — use LLM extraction/classification followed by deterministic Neuron-JS decisioning. ## Run all examples diff --git a/examples/langgraph-decision-node/README.md b/examples/langgraph-decision-node/README.md new file mode 100644 index 0000000..b8edac3 --- /dev/null +++ b/examples/langgraph-decision-node/README.md @@ -0,0 +1,78 @@ +# LangGraph deterministic decision node recipe + +Use this recipe when a LangGraph workflow needs LLM extraction/classification followed by a deterministic Neuron-JS decision. + +Scenario: an LLM classifies a message as a high-risk refund request. Neuron-JS validates the structured context, executes approved JSON rules, and returns `nextNode: "refund-human-review"` with `requiresApproval: true`. + +## Local validation + +From the repository root: + +```bash +yarn build +node examples/langgraph-decision-node/run.ts +``` + +Expected output: + +```json +{ + "ok": true, + "rulesExecuted": 1, + "nextNode": "refund-human-review", + "requiresApproval": true +} +``` + +The local runner intentionally has no LangGraph dependency. It proves the deterministic decision node contract. + +## Copy-paste LangGraph node shape + +```typescript +import { + Neuron, + Synapse, + explainExecution, + summarizeExecutionOutput, + validateExecutionContext, + validateScript, +} from '@sebasoft/neuron-js'; + +export async function neuronDecisionNode(state) { + const script = state.refundRoutingRules; + const context = { + messages: [], + state: { + classification: state.llmClassification, + }, + }; + + const scriptValidation = validateScript(script); + const contextValidation = validateExecutionContext(context); + + if (!scriptValidation.ok || !contextValidation.ok) { + return { + ...state, + neuronDecision: { + ok: false, + errors: [...scriptValidation.errors, ...contextValidation.errors], + }, + }; + } + + const result = new Synapse(new Neuron()).execute(script, context); + const output = summarizeExecutionOutput(result); + const explanation = explainExecution({ script, result }); + + return { + ...state, + neuronDecision: output, + neuronExplanation: explanation, + nextNode: result.context.state.workflow?.nextNode, + }; +} +``` + +## Boundary + +The LLM extracts and classifies. It does not choose the final route from free-form text. Neuron-JS validates the JSON input and produces the deterministic Neuron-JS decision, explanation trace, and stable routing fields used by graph edges. diff --git a/examples/langgraph-decision-node/expected-output.json b/examples/langgraph-decision-node/expected-output.json new file mode 100644 index 0000000..f0314b0 --- /dev/null +++ b/examples/langgraph-decision-node/expected-output.json @@ -0,0 +1,18 @@ +{ + "ok": true, + "rulesExecuted": 1, + "messages": [ + "INFO: Deterministic LangGraph decision set to refund-human-review", + "INFO: Action \"set-langgraph-next-node\" executed successfully", + "INFO: Rule \"route-high-risk-refund\" conditions met and actions executed" + ], + "nextNode": "refund-human-review", + "requiresApproval": true, + "reason": "LLM extraction classified a high-risk refund request; Neuron-JS made the deterministic Neuron-JS decision.", + "explanation": { + "scriptId": "langgraph-refund-decision", + "rulesEvaluated": 1, + "rulesExecuted": 1, + "traceTypes": ["script_received", "rule_available", "execution_completed"] + } +} diff --git a/examples/langgraph-decision-node/input.json b/examples/langgraph-decision-node/input.json new file mode 100644 index 0000000..5a5d925 --- /dev/null +++ b/examples/langgraph-decision-node/input.json @@ -0,0 +1,12 @@ +{ + "messages": [], + "state": { + "classification": { + "intent": "refund_request", + "risk": "high", + "riskScore": 95, + "confidence": 0.91 + }, + "llmBoundary": "LLM extraction/classification only; deterministic routing happens in Neuron-JS." + } +} diff --git a/examples/langgraph-decision-node/rules.json b/examples/langgraph-decision-node/rules.json new file mode 100644 index 0000000..5224051 --- /dev/null +++ b/examples/langgraph-decision-node/rules.json @@ -0,0 +1,44 @@ +{ + "id": "langgraph-refund-decision", + "rules": [ + { + "id": "route-high-risk-refund", + "type": "simple_rule", + "options": {}, + "conditions": [ + { + "id": "confidence-threshold", + "type": "compare_two_numbers", + "options": {}, + "params": [ + { "id": "classification-confidence", "name": "op1", "type": "state_number", "value": "classification.confidence", "options": {} }, + { "id": "confidence-comparison", "name": "comp", "type": "comparator", "value": ">=", "options": {} }, + { "id": "confidence-threshold-value", "name": "op2", "type": "simple_number", "value": "0.9", "options": {} } + ] + }, + { + "id": "risk-score-threshold", + "type": "compare_two_numbers", + "options": {}, + "params": [ + { "id": "risk-score", "name": "op1", "type": "state_number", "value": "classification.riskScore", "options": {} }, + { "id": "risk-comparison", "name": "comp", "type": "comparator", "value": ">=", "options": {} }, + { "id": "risk-threshold-value", "name": "op2", "type": "simple_number", "value": "80", "options": {} } + ] + } + ], + "actions": [ + { + "id": "set-langgraph-next-node", + "type": "set_langgraph_decision", + "options": {}, + "params": [ + { "id": "next-node", "name": "nextNode", "type": "simple_string", "value": "refund-human-review", "options": {} }, + { "id": "approval", "name": "requiresApproval", "type": "simple_string", "value": "true", "options": {} }, + { "id": "reason", "name": "reason", "type": "simple_string", "value": "LLM extraction classified a high-risk refund request; Neuron-JS made the deterministic Neuron-JS decision.", "options": {} } + ] + } + ] + } + ] +} diff --git a/examples/langgraph-decision-node/run.ts b/examples/langgraph-decision-node/run.ts new file mode 100644 index 0000000..7b73562 --- /dev/null +++ b/examples/langgraph-decision-node/run.ts @@ -0,0 +1,184 @@ +import { + ExecutionResult, + MessageType, + Neuron, + Synapse, + explainExecution, + summarizeExecutionOutput, + validateExecutionContext, + validateExecutionExplanation, + validateScript, + type ActionOptions, + type ExecutionContext, + type ParameterInterface, +} from "../../dist/esm/index.js"; +import expectedOutput from "./expected-output.json" with { type: "json" }; +import input from "./input.json" with { type: "json" }; +import script from "./rules.json" with { type: "json" }; + +function readStatePath(context: ExecutionContext, path: string): unknown { + return path.split(".").reduce((current, segment) => { + if (current && typeof current === "object" && segment in current) { + return (current as Record)[segment]; + } + return undefined; + }, context.state); +} + +class StateNumberParameter { + static readonly TYPE = "state_number"; + + readonly id: string; + readonly type: string; + readonly name: string; + readonly value: string; + readonly options: Record; + + constructor( + id: string, + type: string, + name: string, + value: string, + options: Record, + ) { + this.id = id; + this.type = type; + this.name = name; + this.value = value; + this.options = options; + } + + getValue(context: ExecutionContext): number | null { + const value = readStatePath(context, this.value); + return typeof value === "number" ? value : null; + } +} + +class SetLangGraphDecisionAction { + static readonly TYPE = "set_langgraph_decision"; + + readonly id: string; + readonly type: string; + private readonly params: ParameterInterface[]; + readonly options: ActionOptions; + private readonly neuron: Neuron; + + constructor( + id: string, + type: string, + params: ParameterInterface[], + options: ActionOptions, + neuron: Neuron, + ) { + this.id = id; + this.type = type; + this.params = params; + this.options = options; + this.neuron = neuron; + } + + private resolveParam(context: ExecutionContext, name: string): unknown { + const param = this.params.find((item) => item.name === name); + if (!param) return null; + const ParamCtor = this.neuron.getParameter(param.type); + return ParamCtor + ? new ParamCtor( + param.id, + param.type, + param.name, + param.value, + param.options, + param.defaultValue, + ).getValue(context) + : null; + } + + execute(context: ExecutionContext): ExecutionResult { + const nextNode = this.resolveParam(context, "nextNode"); + const requiresApprovalRaw = this.resolveParam(context, "requiresApproval"); + const reason = this.resolveParam(context, "reason"); + + if ( + typeof nextNode !== "string" || + typeof requiresApprovalRaw !== "string" || + typeof reason !== "string" + ) { + return new ExecutionResult(false, context, null, [ + "Invalid LangGraph decision input", + ]); + } + + const requiresApproval = requiresApprovalRaw === "true"; + const nextContext: ExecutionContext = { + ...context, + messages: [ + ...context.messages, + { + type: MessageType.INFO, + text: `LangGraph next node: ${nextNode}; approval required: ${requiresApproval}`, + }, + ], + state: { + ...context.state, + workflow: { nextNode, requiresApproval, reason }, + }, + }; + + return new ExecutionResult(true, nextContext, nextNode, [ + `INFO: Deterministic LangGraph decision set to ${nextNode}`, + ]); + } +} + +const scriptValidation = validateScript(script); +const contextValidation = validateExecutionContext(input); + +if (!scriptValidation.ok || !contextValidation.ok) { + console.error( + JSON.stringify( + { ok: false, errors: [...scriptValidation.errors, ...contextValidation.errors] }, + null, + 2, + ), + ); + process.exit(1); +} + +const neuron = new Neuron(); +neuron.registerParameter(StateNumberParameter.TYPE, StateNumberParameter); +neuron.registerAction(SetLangGraphDecisionAction.TYPE, SetLangGraphDecisionAction); + +const result = new Synapse(neuron).execute(script, input as ExecutionContext); +const summary = summarizeExecutionOutput(result); +const explanation = explainExecution({ script, result }); +const explanationValidation = validateExecutionExplanation(explanation); + +if (!explanationValidation.ok) { + console.error( + JSON.stringify({ ok: false, errors: explanationValidation.errors }, null, 2), + ); + process.exit(1); +} + +const workflow = result.context.state.workflow as + | { nextNode?: string; requiresApproval?: boolean; reason?: string } + | undefined; +const actual = { + ...summary, + nextNode: workflow?.nextNode, + requiresApproval: workflow?.requiresApproval, + reason: workflow?.reason, + explanation: { + scriptId: explanation.scriptId, + rulesEvaluated: explanation.rulesEvaluated, + rulesExecuted: explanation.rulesExecuted, + traceTypes: explanation.trace.map((event) => event.type), + }, +}; + +if (JSON.stringify(actual) !== JSON.stringify(expectedOutput)) { + console.error(JSON.stringify({ expected: expectedOutput, actual }, null, 2)); + process.exit(1); +} + +console.log(JSON.stringify(actual, null, 2)); diff --git a/examples/n8n-code-node/README.md b/examples/n8n-code-node/README.md new file mode 100644 index 0000000..5666171 --- /dev/null +++ b/examples/n8n-code-node/README.md @@ -0,0 +1,83 @@ +# n8n Code node deterministic routing recipe + +Use this recipe when an n8n workflow needs deterministic workflow routing after collecting ticket or customer data. + +Scenario: a support workflow receives a high-priority enterprise ticket with negative sentiment. Neuron-JS validates the JSON rule script and execution context, executes a developer-owned registry, and returns `route: "human-escalation"` with `slaHours: 2`. + +## Local validation + +From the repository root: + +```bash +yarn build +node examples/n8n-code-node/run.ts +``` + +Expected output: + +```json +{ + "ok": true, + "rulesExecuted": 1, + "route": "human-escalation", + "slaHours": 2 +} +``` + +The runner exits non-zero if `rules.json`, `input.json`, explanation output, or `expected-output.json` drift. + +## n8n setup + +Use this in self-hosted or custom n8n environments where external modules are allowed. Keep credentials and side effects in normal n8n nodes. Let Neuron-JS return a route only. + +```bash +npm install @sebasoft/neuron-js +``` + +## Copy-paste Code node snippet + +```typescript +const { + Neuron, + Synapse, + explainExecution, + summarizeExecutionOutput, + validateExecutionContext, + validateScript, +} = require('@sebasoft/neuron-js'); + +const script = $json.rules; +const context = { + messages: [], + state: { + ticket: { + id: $json.ticket.id, + priority: $json.ticket.priority, + customerTier: $json.ticket.customerTier, + sentimentScore: $json.ticket.sentimentScore, + }, + }, +}; + +const scriptValidation = validateScript(script); +const contextValidation = validateExecutionContext(context); + +if (!scriptValidation.ok || !contextValidation.ok) { + return [{ + json: { + ok: false, + errors: [...scriptValidation.errors, ...contextValidation.errors], + }, + }]; +} + +const result = new Synapse(new Neuron()).execute(script, context); +const output = summarizeExecutionOutput(result); +const explanation = explainExecution({ script, result }); + +return [{ json: { ...output, explanation } }]; +``` + +## Boundary + +Neuron-JS is the deterministic rule layer, not arbitrary business-user code execution. Give product or operations teams controlled rule vocabulary, validation, tests, review, audit, rollback, and explanations. Route downstream side effects from the returned decision in n8n. diff --git a/examples/n8n-code-node/expected-output.json b/examples/n8n-code-node/expected-output.json new file mode 100644 index 0000000..8e5d8d8 --- /dev/null +++ b/examples/n8n-code-node/expected-output.json @@ -0,0 +1,18 @@ +{ + "ok": true, + "rulesExecuted": 1, + "messages": [ + "INFO: Deterministic support route set to human-escalation within 2h", + "INFO: Action \"assign-human-escalation\" executed successfully", + "INFO: Rule \"route-high-risk-enterprise-ticket\" conditions met and actions executed" + ], + "route": "human-escalation", + "slaHours": 2, + "reason": "High-priority enterprise ticket with negative sentiment requires deterministic workflow routing.", + "explanation": { + "scriptId": "n8n-support-ticket-routing", + "rulesEvaluated": 1, + "rulesExecuted": 1, + "traceTypes": ["script_received", "rule_available", "execution_completed"] + } +} diff --git a/examples/n8n-code-node/input.json b/examples/n8n-code-node/input.json new file mode 100644 index 0000000..b87a637 --- /dev/null +++ b/examples/n8n-code-node/input.json @@ -0,0 +1,12 @@ +{ + "messages": [], + "state": { + "ticket": { + "id": "SUP-2048", + "priority": 9, + "customerTier": "enterprise", + "sentimentScore": -0.72, + "subject": "Production checkout failure" + } + } +} diff --git a/examples/n8n-code-node/rules.json b/examples/n8n-code-node/rules.json new file mode 100644 index 0000000..b51f856 --- /dev/null +++ b/examples/n8n-code-node/rules.json @@ -0,0 +1,54 @@ +{ + "id": "n8n-support-ticket-routing", + "rules": [ + { + "id": "route-high-risk-enterprise-ticket", + "type": "simple_rule", + "options": {}, + "conditions": [ + { + "id": "priority-threshold", + "type": "compare_two_numbers", + "options": {}, + "params": [ + { "id": "ticket-priority", "name": "op1", "type": "state_number", "value": "ticket.priority", "options": {} }, + { "id": "priority-comparison", "name": "comp", "type": "comparator", "value": ">=", "options": {} }, + { "id": "priority-threshold-value", "name": "op2", "type": "simple_number", "value": "8", "options": {} } + ] + }, + { + "id": "negative-sentiment-threshold", + "type": "compare_two_numbers", + "options": {}, + "params": [ + { "id": "ticket-sentiment", "name": "op1", "type": "state_number", "value": "ticket.sentimentScore", "options": {} }, + { "id": "sentiment-comparison", "name": "comp", "type": "comparator", "value": "<=", "options": {} }, + { "id": "sentiment-threshold-value", "name": "op2", "type": "simple_number", "value": "-0.4", "options": {} } + ] + }, + { + "id": "enterprise-tier-check", + "type": "compare_two_strings", + "options": {}, + "params": [ + { "id": "ticket-tier", "name": "op1", "type": "state_string", "value": "ticket.customerTier", "options": {} }, + { "id": "tier-comparison", "name": "comp", "type": "comparator", "value": "=", "options": {} }, + { "id": "enterprise-tier", "name": "op2", "type": "simple_string", "value": "enterprise", "options": {} } + ] + } + ], + "actions": [ + { + "id": "assign-human-escalation", + "type": "set_support_route", + "options": {}, + "params": [ + { "id": "route-name", "name": "route", "type": "simple_string", "value": "human-escalation", "options": {} }, + { "id": "sla-hours", "name": "slaHours", "type": "simple_number", "value": "2", "options": {} }, + { "id": "reason", "name": "reason", "type": "simple_string", "value": "High-priority enterprise ticket with negative sentiment requires deterministic workflow routing.", "options": {} } + ] + } + ] + } + ] +} diff --git a/examples/n8n-code-node/run.ts b/examples/n8n-code-node/run.ts new file mode 100644 index 0000000..71223bf --- /dev/null +++ b/examples/n8n-code-node/run.ts @@ -0,0 +1,276 @@ +import { + ExecutionResult, + MessageType, + Neuron, + Synapse, + explainExecution, + summarizeExecutionOutput, + validateExecutionContext, + validateExecutionExplanation, + validateScript, + type ActionOptions, + type ExecutionContext, + type ParameterInterface, +} from "../../dist/esm/index.js"; +import expectedOutput from "./expected-output.json" with { type: "json" }; +import input from "./input.json" with { type: "json" }; +import script from "./rules.json" with { type: "json" }; + +function readStatePath(context: ExecutionContext, path: string): unknown { + return path.split(".").reduce((current, segment) => { + if (current && typeof current === "object" && segment in current) { + return (current as Record)[segment]; + } + return undefined; + }, context.state); +} + +class StateNumberParameter { + static readonly TYPE = "state_number"; + + readonly id: string; + readonly type: string; + readonly name: string; + readonly value: string; + readonly options: Record; + + constructor( + id: string, + type: string, + name: string, + value: string, + options: Record, + ) { + this.id = id; + this.type = type; + this.name = name; + this.value = value; + this.options = options; + } + + getValue(context: ExecutionContext): number | null { + const value = readStatePath(context, this.value); + return typeof value === "number" ? value : null; + } +} + +class StateStringParameter { + static readonly TYPE = "state_string"; + + readonly id: string; + readonly type: string; + readonly name: string; + readonly value: string; + readonly options: Record; + + constructor( + id: string, + type: string, + name: string, + value: string, + options: Record, + ) { + this.id = id; + this.type = type; + this.name = name; + this.value = value; + this.options = options; + } + + getValue(context: ExecutionContext): string | null { + const value = readStatePath(context, this.value); + return typeof value === "string" ? value : null; + } +} + +class CompareTwoStringsCondition { + static readonly TYPE = "compare_two_strings"; + + readonly id: string; + readonly type: string; + private readonly params: ParameterInterface[]; + readonly options: Record; + private readonly neuron: Neuron; + + constructor( + id: string, + type: string, + params: ParameterInterface[], + options: Record, + neuron: Neuron, + ) { + this.id = id; + this.type = type; + this.params = params; + this.options = options; + this.neuron = neuron; + } + + private resolveParam(context: ExecutionContext, name: string): unknown { + const param = this.params.find((item) => item.name === name); + if (!param) return null; + const ParamCtor = this.neuron.getParameter(param.type); + return ParamCtor + ? new ParamCtor( + param.id, + param.type, + param.name, + param.value, + param.options, + param.defaultValue, + ).getValue(context) + : null; + } + + execute(context: ExecutionContext): ExecutionResult { + const op1 = this.resolveParam(context, "op1"); + const comp = this.resolveParam(context, "comp"); + const op2 = this.resolveParam(context, "op2"); + + if ( + typeof op1 !== "string" || + typeof comp !== "string" || + typeof op2 !== "string" + ) { + return new ExecutionResult(false, context, false, [ + "Invalid string comparison input", + ]); + } + + if (comp !== "=" && comp !== "!=") { + return new ExecutionResult(false, context, false, [ + `Unsupported string comparator: ${comp}`, + ]); + } + + const matched = comp === "=" ? op1 === op2 : op1 !== op2; + return new ExecutionResult(true, context, matched); + } +} + +class SetSupportRouteAction { + static readonly TYPE = "set_support_route"; + + readonly id: string; + readonly type: string; + private readonly params: ParameterInterface[]; + readonly options: ActionOptions; + private readonly neuron: Neuron; + + constructor( + id: string, + type: string, + params: ParameterInterface[], + options: ActionOptions, + neuron: Neuron, + ) { + this.id = id; + this.type = type; + this.params = params; + this.options = options; + this.neuron = neuron; + } + + private resolveParam(context: ExecutionContext, name: string): unknown { + const param = this.params.find((item) => item.name === name); + if (!param) return null; + const ParamCtor = this.neuron.getParameter(param.type); + return ParamCtor + ? new ParamCtor( + param.id, + param.type, + param.name, + param.value, + param.options, + param.defaultValue, + ).getValue(context) + : null; + } + + execute(context: ExecutionContext): ExecutionResult { + const route = this.resolveParam(context, "route"); + const slaHours = this.resolveParam(context, "slaHours"); + const reason = this.resolveParam(context, "reason"); + + if ( + typeof route !== "string" || + typeof slaHours !== "number" || + typeof reason !== "string" + ) { + return new ExecutionResult(false, context, null, [ + "Invalid support route input", + ]); + } + + const nextContext: ExecutionContext = { + ...context, + messages: [ + ...context.messages, + { type: MessageType.INFO, text: `n8n route: ${route} within ${slaHours}h` }, + ], + state: { + ...context.state, + workflow: { route, slaHours, reason }, + }, + }; + + return new ExecutionResult(true, nextContext, route, [ + `INFO: Deterministic support route set to ${route} within ${slaHours}h`, + ]); + } +} + +const scriptValidation = validateScript(script); +const contextValidation = validateExecutionContext(input); + +if (!scriptValidation.ok || !contextValidation.ok) { + console.error( + JSON.stringify( + { ok: false, errors: [...scriptValidation.errors, ...contextValidation.errors] }, + null, + 2, + ), + ); + process.exit(1); +} + +const neuron = new Neuron(); +neuron.registerParameter(StateNumberParameter.TYPE, StateNumberParameter as any); +neuron.registerParameter(StateStringParameter.TYPE, StateStringParameter as any); +neuron.registerCondition(CompareTwoStringsCondition.TYPE, CompareTwoStringsCondition as any); +neuron.registerAction(SetSupportRouteAction.TYPE, SetSupportRouteAction); + +const result = new Synapse(neuron).execute(script, input as ExecutionContext); +const summary = summarizeExecutionOutput(result); +const explanation = explainExecution({ script, result }); +const explanationValidation = validateExecutionExplanation(explanation); + +if (!explanationValidation.ok) { + console.error( + JSON.stringify({ ok: false, errors: explanationValidation.errors }, null, 2), + ); + process.exit(1); +} + +const workflow = result.context.state.workflow as + | { route?: string; slaHours?: number; reason?: string } + | undefined; +const actual = { + ...summary, + route: workflow?.route, + slaHours: workflow?.slaHours, + reason: workflow?.reason, + explanation: { + scriptId: explanation.scriptId, + rulesEvaluated: explanation.rulesEvaluated, + rulesExecuted: explanation.rulesExecuted, + traceTypes: explanation.trace.map((event) => event.type), + }, +}; + +if (JSON.stringify(actual) !== JSON.stringify(expectedOutput)) { + console.error(JSON.stringify({ expected: expectedOutput, actual }, null, 2)); + process.exit(1); +} + +console.log(JSON.stringify(actual, null, 2)); diff --git a/package.json b/package.json index 41c13f0..81ecdea 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tshy", "dev": "vitest", "test": "vitest run", - "examples": "yarn build && node examples/pricing-rules/run.ts && node examples/eligibility-check/run.ts && node examples/workflow-routing/run.ts", + "examples": "yarn build && node examples/pricing-rules/run.ts && node examples/eligibility-check/run.ts && node examples/workflow-routing/run.ts && node examples/n8n-code-node/run.ts && node examples/langgraph-decision-node/run.ts", "lint": "biome check .", "format": "biome format --write .", "prepare": "yarn build", diff --git a/tests/contracts/workflow-integrations.test.ts b/tests/contracts/workflow-integrations.test.ts new file mode 100644 index 0000000..cca0d6b --- /dev/null +++ b/tests/contracts/workflow-integrations.test.ts @@ -0,0 +1,67 @@ +import { existsSync, readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { + validateExecutionContext, + validateScript, +} from "../../src/index.js"; + +const requiredFiles = [ + "docs/integrations/index.md", + "docs/integrations/n8n.md", + "docs/integrations/langgraph.md", + "examples/n8n-code-node/README.md", + "examples/n8n-code-node/rules.json", + "examples/n8n-code-node/input.json", + "examples/n8n-code-node/expected-output.json", + "examples/n8n-code-node/run.ts", + "examples/langgraph-decision-node/README.md", + "examples/langgraph-decision-node/rules.json", + "examples/langgraph-decision-node/input.json", + "examples/langgraph-decision-node/expected-output.json", + "examples/langgraph-decision-node/run.ts", +]; + +function readJson(path: string): unknown { + return JSON.parse(readFileSync(path, "utf8")); +} + +describe("NJS-GROWTH-06 workflow automation recipes", () => { + it.each(requiredFiles)("publishes %s", (filePath) => { + expect(existsSync(filePath)).toBe(true); + }); + + it("validates recipe scripts and execution contexts before runtime", () => { + for (const exampleDir of [ + "examples/n8n-code-node", + "examples/langgraph-decision-node", + ]) { + expect(validateScript(readJson(`${exampleDir}/rules.json`)), exampleDir).toEqual({ ok: true, errors: [] }); + expect(validateExecutionContext(readJson(`${exampleDir}/input.json`)), exampleDir).toEqual({ ok: true, errors: [] }); + } + }); + + it("documents deterministic routing, LLM boundaries, validation, and explanations", () => { + const n8n = readFileSync("docs/integrations/n8n.md", "utf8"); + const langgraph = readFileSync("docs/integrations/langgraph.md", "utf8"); + const combined = `${n8n}\n${langgraph}`; + + expect(combined).toContain("validateScript"); + expect(combined).toContain("validateExecutionContext"); + expect(combined).toContain("summarizeExecutionOutput"); + expect(combined).toContain("explainExecution"); + expect(n8n).toContain("deterministic workflow routing"); + expect(langgraph).toContain("LLM extraction"); + expect(langgraph).toContain("deterministic Neuron-JS decision"); + }); + + it("links integration recipes from AI-readable surfaces", () => { + const llms = readFileSync("docs/public/llms.txt", "utf8"); + const llmsFull = readFileSync("docs/public/llms-full.txt", "utf8"); + const skill = readFileSync("docs/public/skills/neuron-js/SKILL.md", "utf8"); + + for (const content of [llms, llmsFull, skill]) { + expect(content).toContain("n8n-code-node"); + expect(content).toContain("langgraph-decision-node"); + } + }); +}); From 32eeaf051158c481e3e02f7ad41be665f6634893 Mon Sep 17 00:00:00 2001 From: SebaSOFT Date: Mon, 22 Jun 2026 13:28:22 +0000 Subject: [PATCH 2/2] [verified] docs: tighten LangGraph recipe classification checks --- examples/langgraph-decision-node/rules.json | 20 +++++ examples/langgraph-decision-node/run.ts | 98 ++++++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/examples/langgraph-decision-node/rules.json b/examples/langgraph-decision-node/rules.json index 5224051..53dd8d9 100644 --- a/examples/langgraph-decision-node/rules.json +++ b/examples/langgraph-decision-node/rules.json @@ -6,6 +6,26 @@ "type": "simple_rule", "options": {}, "conditions": [ + { + "id": "intent-check", + "type": "compare_two_strings", + "options": {}, + "params": [ + { "id": "classification-intent", "name": "op1", "type": "state_string", "value": "classification.intent", "options": {} }, + { "id": "intent-comparison", "name": "comp", "type": "comparator", "value": "=", "options": {} }, + { "id": "refund-intent", "name": "op2", "type": "simple_string", "value": "refund_request", "options": {} } + ] + }, + { + "id": "risk-check", + "type": "compare_two_strings", + "options": {}, + "params": [ + { "id": "classification-risk", "name": "op1", "type": "state_string", "value": "classification.risk", "options": {} }, + { "id": "risk-comparison", "name": "comp", "type": "comparator", "value": "=", "options": {} }, + { "id": "high-risk", "name": "op2", "type": "simple_string", "value": "high", "options": {} } + ] + }, { "id": "confidence-threshold", "type": "compare_two_numbers", diff --git a/examples/langgraph-decision-node/run.ts b/examples/langgraph-decision-node/run.ts index 7b73562..05a973b 100644 --- a/examples/langgraph-decision-node/run.ts +++ b/examples/langgraph-decision-node/run.ts @@ -54,6 +54,100 @@ class StateNumberParameter { } } +class StateStringParameter { + static readonly TYPE = "state_string"; + + readonly id: string; + readonly type: string; + readonly name: string; + readonly value: string; + readonly options: Record; + + constructor( + id: string, + type: string, + name: string, + value: string, + options: Record, + ) { + this.id = id; + this.type = type; + this.name = name; + this.value = value; + this.options = options; + } + + getValue(context: ExecutionContext): string | null { + const value = readStatePath(context, this.value); + return typeof value === "string" ? value : null; + } +} + +class CompareTwoStringsCondition { + static readonly TYPE = "compare_two_strings"; + + readonly id: string; + readonly type: string; + private readonly params: ParameterInterface[]; + readonly options: ActionOptions; + private readonly neuron: Neuron; + + constructor( + id: string, + type: string, + params: ParameterInterface[], + options: ActionOptions, + neuron: Neuron, + ) { + this.id = id; + this.type = type; + this.params = params; + this.options = options; + this.neuron = neuron; + } + + private resolveParam(context: ExecutionContext, name: string): unknown { + const param = this.params.find((item) => item.name === name); + if (!param) return null; + const ParamCtor = this.neuron.getParameter(param.type); + return ParamCtor + ? new ParamCtor( + param.id, + param.type, + param.name, + param.value, + param.options, + param.defaultValue, + ).getValue(context) + : null; + } + + execute(context: ExecutionContext): ExecutionResult { + const op1 = this.resolveParam(context, "op1"); + const comp = this.resolveParam(context, "comp"); + const op2 = this.resolveParam(context, "op2"); + + if ( + typeof op1 !== "string" || + typeof comp !== "string" || + typeof op2 !== "string" + ) { + return new ExecutionResult(false, context, false, [ + "Invalid string comparison input", + ]); + } + + if (comp !== "=" && comp !== "!=") { + return new ExecutionResult(false, context, false, [ + `Unsupported string comparator: ${comp}`, + ]); + } + + const matched = comp === "=" ? op1 === op2 : op1 !== op2; + return new ExecutionResult(true, context, matched); + } +} + class SetLangGraphDecisionAction { static readonly TYPE = "set_langgraph_decision"; @@ -145,7 +239,9 @@ if (!scriptValidation.ok || !contextValidation.ok) { } const neuron = new Neuron(); -neuron.registerParameter(StateNumberParameter.TYPE, StateNumberParameter); +neuron.registerParameter(StateNumberParameter.TYPE, StateNumberParameter as any); +neuron.registerParameter(StateStringParameter.TYPE, StateStringParameter as any); +neuron.registerCondition(CompareTwoStringsCondition.TYPE, CompareTwoStringsCondition as any); neuron.registerAction(SetLangGraphDecisionAction.TYPE, SetLangGraphDecisionAction); const result = new Synapse(neuron).execute(script, input as ExecutionContext);