# FlowRunner Extension Development

This is the reference for building a FlowRunner service extension. It describes the one supported way to
declare a service — use it both for new services and for changes to existing ones.

> **This tree is self-contained.** It ships as an npm module, so every link must be relative and must stay
> inside this folder. Never link out to the FlowRunner console repo — if a fact from there matters, state the
> fact here rather than pointing at it.

> **This reference is complete, and it is the only source.** Everything needed to build a service — the
> declaration surface, the runtime behaviour it depends on, and the naming and layout rules — is written
> down here. So: **never read the `flowrunner-cli` source, its bundled runtime, its type definitions or its
> templates**, and never infer a rule from a neighbouring service or from what an API "probably" does.
>
> If you cannot find something here, that is a defect in this reference, not an invitation to go and look.
> Stop, state what is missing, and proceed on the documented behaviour — flagging plainly that you did.
> Guessing produces a service that looks right and is wrong in a way nobody sees until it is deployed.

> **These are authoring docs — how to *write* a service.** Runtime behaviour appears here only where it
> changes what you should write: the validation guarantees in [07-actions.md](07-actions.md), the trigger
> lifecycles in [08](08-polling-triggers.md)/[09](09-realtime-triggers.md), the service model in
> [11-output-model.md](11-output-model.md), and the runtime contracts in
> [13-runtime-contracts.md](13-runtime-contracts.md).

A service is a single JavaScript module in `services/{id}/src/index.js` that builds an extension with the
global `Flowrunner.createExtension(...)` factory. Everything is declared through factory props and zod
schemas (`Flowrunner.z`). Declaring a service produces the platform's service model
(`SharedExtensionServiceInfo` + `methodsMap` + config descriptions), and the `handleTrigger*` SYSTEM methods
are the runtime seam the platform drives.

## Working on an existing service

Read its `src/index.js` first and follow what is already there — its `id` values, categories, labels and
param names are live contract, not style choices.

## Identity & naming

- **Identity:** every registered item (action / trigger / dictionary) is keyed by **`id`** (not `name`).
  `id` is persisted and drives execution — once a service is deployed, an `id` can never change, and an
  existing service's `id`s are authoritative even when they read oddly. `name` is the service display label
  only; per-item human text is `label`.
- **Uniqueness:** ids must be unique across *all* item kinds — see
  [01-service-structure.md](01-service-structure.md).
- **Naming:** a dictionary `id` starts with `get` and ends with `Dictionary` (`getTablesDictionary`), and
  the const holding the ref carries the same name; reused param schemas end `Param` (`appIdParam`).

## Reading order

**Decide the feature set first, then load in one batch.** Every document you load stays in context for the
rest of the task and is re-read on every subsequent turn, so loading all eighteen to build a three-action
service is the single most expensive mistake available here. Loading one you turn out to need *late* is
worse still — it costs a rewrite. So: settle what the service will contain, then read the core plus the
conditional groups that apply, together.

### Core — read for every service

| # | Document | Covers |
|---|----------|--------|
| 1 | [01-service-structure.md](01-service-structure.md) | `Flowrunner.createExtension(props)`: id/name/logo/colors, `usesFileStorage`, `configItems`, `initContext`, `apiRequest`, `helpers`; folder layout |
| 2 | [02-zod-types.md](02-zod-types.md) | zod types & plugins — widget derived from type; `.label`/`.describe`/`.example`/`.optional`/`.default`/`.shared`/`.dictionary`/`.map` |
| 3 | [03-parameters.md](03-parameters.md) | `params` (all body params), the platform's own file-options groups — one per Files call — and `dynamicParams` (runtime-generated fields) |
| 4 | [04-http-requests.md](04-http-requests.md) | the shared `apiRequest` helper, and the `Flowrunner.Request` client under it — sending model, methods, query, headers, bodies, multipart, responses, `ResponseError` |
| 5 | [05-dictionaries.md](05-dictionaries.md) | `ext.dictionary(props)` — dynamic param options; `label` (→ `operationName`, always set it), `criteria` → auto `dependsOn`, optional `result` sample |
| 6 | [07-actions.md](07-actions.md) | `ext.addAction(props)`, the shared `execute` context bag, and input validation |
| 7 | [16-gotchas.md](16-gotchas.md) | Cross-cutting traps that fail *silently* — empty-string params, lookup tables keyed by user input, dictionary error handling. Deliberately short |
| 8 | [14-reference-service.md](14-reference-service.md) | A complete verified service exercising the whole surface — **read this instead of the CLI templates** |

Dictionaries are core because nearly every service has at least one id-like parameter, and a flow author
picking a value from a list is the difference between a usable service and one that requires them to go
and look an id up by hand. Assume you need [05](05-dictionaries.md) until you have established that no
parameter can be backed — and note that "the API has no endpoint that lists these" does **not** settle it;
see *Browse versus resolve* in that document.

### Conditional — read the group that applies

Add whole groups, decided from the feature set, before you start writing.

**OAuth2** — [06-oauth2.md](06-oauth2.md)
Read it when the API authenticates with OAuth2. **Skip it entirely for API-key, bearer-token or
URL-embedded-token auth** — those need only `configItems` + `initContext` from [01](01-service-structure.md).

**Triggers** — [08-polling-triggers.md](08-polling-triggers.md) · [09-realtime-triggers.md](09-realtime-triggers.md)
Skip both when the service is actions-only. Otherwise read the one that matches how the API delivers
changes: 08 for polling, 09 for webhooks. Read both only while you are deciding between them — and note
that some APIs make the two mutually exclusive, in which case the decision comes first and the loser is
dead weight.

**Files** — [10-files-api.md](10-files-api.md)
Read it when any parameter takes a file or an action returns one. It also states which file scopes a method
can use depending on whether it runs inside a flow execution, so read it before declaring `usesFileStorage`.
Letting the flow author choose how a file is stored, listed, fetched or deleted is a platform param group,
not something you declare — one per Files call. See *Platform param schemas* in
[03-parameters.md](03-parameters.md).

**Client certificates** — [17-client-certificates.md](17-client-certificates.md)
Read it when the API requires mutual TLS. `.cert()` belongs in `apiRequest`, and the certificate is needed
for the OAuth2 token call too, so decide this before writing the auth layer.

**Verifying the build** — [13-runtime-contracts.md](13-runtime-contracts.md) · [11-output-model.md](11-output-model.md)
Read 13 at the build-check step, or earlier if a runtime fact is unclear (where `.default()` is applied,
which SYSTEM-method contracts are exercisable locally). Read
[11](11-output-model.md) only when you need to know how a declaration surfaces to the platform — it is a
debugging aid, not something you need in order to write a service correctly.

**Service README** — [12-service-readme.md](12-service-readme.md)
Read it at the step where you write the service `README.md`, not up front.

**Unit tests** — [15-unit-tests.md](15-unit-tests.md) · [18-test-recipes.md](18-test-recipes.md)
Read 15 when you write or fix a suite; add [18](18-test-recipes.md) for the kinds the service actually
registers — dictionaries, triggers, files — and skip it entirely for an actions-only service. `flowrunner init` copies a unit-test harness into the project —
`sandbox/` at the root, one suite per service in `services/{id}/tests/` — which runs a service exactly as
the platform does with HTTP intercepted at the socket. It is a separate document because writing a service
and testing one are separate jobs; do not read it while writing a service.

That is also separate from **checking that a service builds**, which needs no suite at all and is part of
writing one: `require('flowrunner-cli/runtime')` exports `buildServiceDefinition`, `getServiceConfigs`,
`runServiceMethod`, `detectVersion`, `resetRegistry` and `parseJsDocToServiceJson`, and importing it installs
the `Flowrunner` global (`Request`, `z`, `crypto`, `createExtension`, `getExtension`, `ServerCode`).
Signatures, a copy-paste snippet, and the boundary between a throwaway check and a suite are in
[13-runtime-contracts.md](13-runtime-contracts.md).
