# Two patterns, five services, one n8n workflow

> Source: <https://dev.to/hidekimori/two-patterns-five-services-one-n8n-workflow-a4>
> Published: 2026-06-17 13:00:00+00:00

The first two articles in this series each showed one technique. [Implementation notes #001](https://dev.to/hidekimori/build-a-multi-step-n8n-form-with-dynamic-dropdowns-no-plugin-needed-1pm4) was a dynamic dropdown — a form field that fills itself from an API. [Implementation notes #002](https://dev.to/hidekimori/let-your-n8n-template-ask-for-the-users-api-key-54n9) was a dynamic credential — an API key that arrives from the form and threads through to the HTTP nodes.

This article is the capstone. It walks through `all-services-demo`

, the example workflow that ships with `n8n-nodes-ldxhub`

, where those two techniques combine with a Switch node to host five different AI document-processing services inside one workflow — structured extraction, translation refinement, OCR, PDF conversion, and text extraction.

The screenshots and the workflow JSON below come from the

`n8n-nodes-ldxhub`

package. The patterns themselves are generic — they work for any set of services you want to consolidate into a single template.

This is not a "follow these steps" article. It's a parts catalog. No two readers are solving the same problem, and templates rarely fit anyone's situation as-is. Take what fits. Drop the rest. You don't need to understand all 46 nodes to reuse the patterns.

The workflow has 46 nodes — large enough to look intimidating in the editor, but structurally it's just five repeated paths plus a small routing section.

The entry section is two nodes:

Everything to the right of the Switch is service-specific. Five paths fan out: StructFlow, RefineLoop, RenderOCR, CastDoc, ExtractDoc. Each path ends in two Form Ending nodes — one for success (auto-downloads the result), one for error.

That's the spine: form → switch → service path → ending. The complexity is pushed into the service paths.

The Switch node ("Route by Service") uses Rules mode. Each rule reads the same expression from the form — `{{ $json.service }}`

— and compares it to a static service name.

```
Rule 1:  {{ $json.service }}  is equal to  structflow   → output: structflow
Rule 2:  {{ $json.service }}  is equal to  refineloop   → output: refineloop
Rule 3:  {{ $json.service }}  is equal to  renderocr    → output: renderocr
Rule 4:  {{ $json.service }}  is equal to  castdoc      → output: castdoc
Rule 5:  {{ $json.service }}  is equal to  extractdoc   → output: extractdoc
```

The read side is dynamic (the expression resolves to whatever the user picked). The match side is static (fixed strings). That asymmetry is intentional. Static rules mean adding a new service is a manual edit — open the Switch node, add a row, save. No regeneration, no template hooks, no auto-discovery. Boring and unsurprising.

This is the part you can lift cleanly: a Switch with N static rules driven by one expression from upstream. It works for service routing, document type routing, user tier routing, anything that fans into discrete branches.

Once you start reading the service paths, you notice something: they are not all the same shape. There are two distinct patterns.

```
Get Models (HTTP)
  → Derive Options (Set)
    → Run Form (Form, next page)
      → Inject Binary (Code)
        → Run (LDX hub)
          → Download / Error (Form Endings)
```

One form page collects everything the user needs to choose. The model selection, the input, the parameters — all in one screen. There's only one form page after the Switch.

```
Get Engines (HTTP)
  → Select Engine (Form, next page)
    → Derive Options (Set)
      → Upload File (Form, next page)
        → Filter by File (Set)
          → Select Output (Form, next page)
            → Inject Binary (Code)
              → Run (LDX hub)
                → Download / Error (Form Endings)
```

Three form pages, each gated on the previous one. First the engine is chosen. Then the file is uploaded. Then the output format is selected — and the available outputs are filtered based on what the chosen engine supports for the uploaded file type. The `Filter by File`

Set node sits in the middle of that dependency.

The shape of the path follows the shape of the user's decisions. When the choices are independent — pick a model, pass some data — one form page is enough. When the choices cascade — engine restricts file types, file restricts output formats — the form has to be split, and intermediate Set nodes have to filter the options between pages.

I tried to force a single pattern across all five services. It made the simpler services more complicated than they needed to be. The honest design was to let the cascading services be longer, accept the asymmetry, and document it.

Two patterns isn't a sign of incompleteness. It's the consolidation accepting that two shapes were genuinely warranted.

Walking through StructFlow gives you the vocabulary for all five paths. The other four are variations.

**Get Models** — an HTTP node that hits `/structflow/models`

and returns the available LLMs (gpt-5.5, claude-sonnet-4-6, gemini-3-flash, etc.). This is the data source for the dropdown.

**Derive Options** — a Set node that reshapes the model list into the format n8n's Form trigger wants for a dropdown: `[{name, value}, ...]`

. Same trick as in [#001](https://dev.to/hidekimori/build-a-multi-step-n8n-form-with-dynamic-dropdowns-no-plugin-needed-1pm4) — derive the dropdown from data, not from a hardcoded list.

**Run Form** — a single form page that asks for everything: which model to use, the input data, any parameters. The "model" dropdown reads its options from the upstream `Derive Options`

node.

**Inject Binary** — a Code node that does one thing. In n8n, uploaded files travel through the workflow as binary data, separate from the JSON fields, and some intermediate nodes (like Set) only preserve the JSON side. By the time data reaches the LDX hub node, the binary part has been dropped.

``` js
return $input.all().map(item => ({
  json: item.json,
  binary: $('StructFlow: Run Form').item.binary
}));
```

The Code node reaches back to the form node and re-attaches the binary. One line of glue, but without it the file disappears.

**Run** — the LDX hub custom node, with `runJob: structFlow`

. This is where the API call actually happens. The credential is set in expression mode to read from the form input — the dynamic credential pattern from [#002](https://dev.to/hidekimori/let-your-n8n-template-ask-for-the-users-api-key-54n9).

**Download / Error** — two Form Ending nodes. The Run node has two output ports: Success goes to Download (which serves the result file), Error goes to Error (which shows the error message).

Five other paths follow the same idea. The names change, the number of form pages changes, but the role of each node is the same.

All five service paths end at the same LDX hub custom node. Same node type, same credential, same shape — only the `runJob`

parameter differs:

```
StructFlow: Run    →  runJob: structFlow
RefineLoop: Run    →  runJob: refineLoop
RenderOCR: Run     →  runJob: renderOcr
CastDoc: Run       →  runJob: castDoc
ExtractDoc: Run    →  runJob: extractDoc
```

This is the abstraction the custom node provides. From the workflow's perspective, "running a service" looks identical across the five paths. The differences are buried inside the node's implementation, where they belong.

This generalizes the idea from [#002's sidebar](https://dev.to/hidekimori/let-your-n8n-template-ask-for-the-users-api-key-54n9): a custom node that hides its variations behind a uniform interface lets you compose it freely. Five services. One credential type. One node. Five jobs. The workflow author doesn't have to know how StructFlow differs from RenderOCR — the node knows.

If you're building your own custom node, this is the shape worth aiming for. One node, parameterized by job kind. The workflow stays clean. The variations stay encapsulated.

Every service path has two endpoints: Download (success) and Error. Both are Form Ending nodes. Both are visible to the user. This isn't decorative.

A distributable template has one minimum obligation: once the user clicks Submit, they need to see *what happened*. If the call succeeded, they get the result. If it failed, they get the error. There's no third state where the form just ends silently.

Earlier failures — the Get Models call returning empty, the Inject Binary node crashing — are not handled. Those are skipped here because the template is meant to be minimal, and because adding error endings everywhere makes the canvas unreadable. But the final Run node's error branch is mandatory. That's the one place where the user's expectation ("I started a job, what happened?") has to be answered.

Where there's an `if`

, you need an `else`

. The else doesn't have to be elegant. It just has to exist.

This is the part you can lift on its own: any time a workflow has a user-visible "run" step, pair its success with an error ending. Other branches can be skipped or logged, but the user-facing one is non-negotiable.

The technique parts above — Switch routing, two form patterns, Binary injection, Run convergence, if/else error pairing — are each portable. You can lift them one at a time. But the article would be missing something if it stopped there.

Bringing five services into one workflow is itself the work. Not a tutorial-friendly kind of work, because nothing in this section is a discrete technique. It's a series of small decisions:

`<Service>: <Role>`

everywhere — every reader knows where they are).`runJob`

parameter — the spine stays uniform.None of these are clever. Each is the obvious decision once you've seen the alternatives. But the obvious decisions are what hold the template together.

There's a particular kind of reader this article is also for: the one who doesn't need the technique, who just needs a working template they can drop into n8n and run. The consolidation work is the deliverable for them. The fact that the article also explains how it works is a side benefit.

This contrasts with a different design philosophy — the one that spreads concerns across many separate sub-workflows, each handling a narrow responsibility. In larger n8n installations with several maintainers, you might split these responsibilities into reusable sub-workflows, with each service called via the Execute Workflow node. For a solo-maintained distributable template intended to be lifted and adapted, the consolidated shape was easier to understand and ship. Fewer moving parts, fewer integration points, one place to read.

This is parts.

If you read this article and take the whole workflow, run it as-is, that's fine. If you take only the Switch routing and rebuild every service path from scratch, that's better in many cases. If you take only the Inject Binary trick because that's what bit you yesterday, that's the best use of this article.

No two requirements are identical. Every reader is solving a different problem. A template that pretends to be a one-size answer would be lying. A template that is honest about being a parts catalog — here are the pieces, here is how they fit together, take what fits — is a different kind of useful thing.

That's what `all-services-demo`

is meant to be. That's what this article is meant to be.

The [n8n-nodes-ldxhub package](https://www.npmjs.com/package/n8n-nodes-ldxhub) ships `examples/all-services-demo.json`

alongside the node code. Import it into your n8n instance, add an [LDX hub API key](https://gw.portal.ldxhub.io) (free tier: 25,000 credits/month, no card), and the workflow runs. Open the JSON and lift parts into your own templates.

This closes Phase 1 of Implementation notes — three articles, three angles on the same theme: how an n8n workflow becomes a small, distributable thing. The next phase will pick up other corners worth writing down.
