# 01 — Service Structure

A service is one JavaScript module per folder — `services/{id}/src/index.js` — that calls the global
`Flowrunner.createExtension(props)` factory and then registers actions / dictionaries / triggers on the
returned `ext`. Everything is declared through the factory props and zod schemas.

## Folder layout

```
services/{id}/
├── package.json        the service's OWN manifest — its dependencies
├── package-lock.json
├── node_modules/       installed here; ships inside the deployed package
├── src/
│   └── index.js        the service module — all logic lives here
├── public/
│   └── icon.svg        the logo, and any other publicly served asset
└── README.md           ships with the service (see 12-service-readme.md)
```

## A service is its own npm package

**`services/{id}/` has its own `package.json`, and dependencies are installed inside it.** The deployed
package is that folder — its source, its manifest, its `node_modules` — and nothing from the project root
travels with it.

So to add a dependency to a service, install it **in the service's folder**:

```bash
cd services/{id}
npm install axios
```

Installing at the project root instead is the mistake to avoid: the module resolves fine while you are
testing locally, because node walks up the directory tree — and then the deployed service throws
`MODULE_NOT_FOUND` at its first execution, because the root `node_modules` was never packed.

The project root keeps its own `package.json` for the tooling — the CLI and the test harness. That one is
not deployed.

**The icon belongs in `public/`, not beside `src/`.** Only `public/` is served: the platform exposes it at
`…/{serviceId}/file/{path}`, rooted at that folder and refusing to escape it. A file at the service root is
never reachable, so a logo placed there resolves to a 404 and the service renders with no icon — a failure
that shows up only after deploy, not at build time.

`logo` is then the path *within* `public/`. A leading slash is stripped, so `'/icon.svg'` and `'icon.svg'`
are equivalent and both mean `public/icon.svg`; the leading-slash form is the convention. Subfolders work
(`'logos/icon.svg'` → `public/logos/icon.svg`). An absolute `https://` URL is **not** supported — the value
is always resolved against `public/`.

Actions and triggers accept their own optional `logo`, resolved the same way, which overrides the service
logo for that one item. Omit it and the item inherits the service's.

## `Flowrunner.createExtension(props)`

| Property | Type | Req | Description |
|---|---|---|---|
| `id` | string | ✓ | Stable service id (→ `SharedExtensionServiceInfo.id`). |
| `name` | string | ✓ | Human display name. |
| `description` | string | ✓ | Service description. |
| `logo` | string | ✓ | Icon path, resolved **inside the service's `public/` folder** — see [Folder layout](#folder-layout). |
| `appearanceColors` | `[string, string]` | – | `[primary, secondary]`. |
| `usesFileStorage` | boolean | – | `true` provisions `ctx.files`; when false, `ctx.files` is a proxy that throws. Default false. See [10-files-api.md](10-files-api.md). |
| `configItems` | `z.object` | – | App + shared config (see [02-zod-types.md](02-zod-types.md)). → `appConfigDescriptions` / `sharedConfigDescriptions`. Omit when the service needs none; a plain object of fields is **not** accepted (see [03-parameters.md](03-parameters.md)). |
| `initContext` | fn | – | `({ config, oauth }) => context`; runs once per invocation. Runs for OAuth flows too (`oauth` may be empty). |
| `apiRequest` | fn | – | Shared HTTP helper (envelope-unwrap, error-normalize, redacted logging). See [04-http-requests.md](04-http-requests.md). |
| `helpers` | fn | – | ctx factory `(ctx) => ({ ... })` for **genuinely reusable service logic**; result exposed as `helpers` in every handler bag. `ctx` = `{ config, context, oauth, apiRequest, files, filesScope, logger }` (no `params`). |

```js
const ext = Flowrunner.createExtension({
  id: 'svc', name: 'Svc', description: '…', logo: '/icon.png', appearanceColors: ['#111','#222'],
  usesFileStorage: false,
  configItems: z.object({ apiKey: z.string().label('API Key') }),
  initContext: ({ config, oauth }) => ({ apiKey: config.apiKey, accessToken: oauth?.accessToken }),
  apiRequest : async ({ url, method = 'get', body, query, context }) =>
    new Flowrunner.Request(url, method, body).set({ Authorization: `Bearer ${ context.apiKey }` }).query(query),
  helpers    : ({ apiRequest }) => ({ collectPages: (url) => collectPages(apiRequest, url) }),
})
```

## Identity & naming

- 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. `name` is the
  service display label only; per-item human text is `label`.
- **Ids must be unique across *all* kinds**, not just within one — an action and a dictionary cannot share
  an id, and registration fails with `FR_EXT_DUPLICATE_ITEM_ID` if they do. Generated loader names count
  too: an action `foo` with a loader `result` claims `foo_SampleResultLoader`, and `dynamicParams` claims
  `foo_DynamicParams`, so don't register anything under those names by hand.
- **A dictionary `id` starts with `get` and ends with `Dictionary`** — `getChatsDictionary`,
  `getBoardsDictionary`. Name the const holding the ref the same as the id, so the declaration and every
  `.dictionary(ref)` read alike.
- Reused param schemas end `Param` (`appIdParam`).

## `initContext` and `helpers`

- `initContext({ config, oauth })` runs once per invocation and returns the `context` object handed to
  every handler — the place to derive base URLs, resolved credentials and anything else every handler needs.
  It also runs during OAuth flows, where `oauth` may be empty.
- `helpers(ctx)` builds reusable bound helpers exposed as `helpers` in each handler's context bag; its
  `ctx` is `{ config, context, oauth, apiRequest, files, filesScope, logger }` (no `params`).

**Put real service logic in `helpers`, not plumbing.** A helper earns its place when several handlers share
non-trivial behaviour — paging a list endpoint to the end, resolving an id through two calls, normalising a
paginated envelope. It does **not** earn its place by wrapping something the bag already provides:

| ✗ Not a helper | Why |
|---|---|
| `call: (method, body) => apiRequest({ url: `/${ method }`, method: 'post', body })` | `apiRequest` is already in every handler bag. A rename adds a layer to read through and hides the URL each action calls. |
| `upload: (...) => apiRequest(...)` with a form | Same — it is `apiRequest` with a different body. Build the form in the action, or in a module-level function. |
| `readFile` / `storeFile` wrapping `files.*` | `files` is already in the bag ([10-files-api.md](10-files-api.md)). Wrapping it buys nothing and obscures which scope was used. |

If the thing you want to share needs no `ctx` at all, it is not a helper either — make it a module-level
function (see below).

## Module layout

One file, read top to bottom in the order the platform resolves it:

```js
const z = Flowrunner.z                 // 1. the zod handle

const ext = Flowrunner.createExtension({ … })   // 2. the factory: config, initContext, apiRequest, helpers

const getBoardsDictionary = ext.dictionary({ … })   // 3. dictionaries — declared before the params that use them

const boardIdParam = z.string()        // 4. shared param schemas
  .label('Board')
  .dictionary(getBoardsDictionary)

ext.addAction({ … })                   // 5. actions
ext.addPollingTrigger({ … })           // 6. triggers
ext.addRealtimeTrigger({ … })

// 7. helper functions, at the end
function asInteger(value, label) {
  …
}
```

**Module-level helpers are function declarations, and they go at the bottom.** Write
`function asInteger(value, label) { … }`, not `const asInteger = (value, label) => { … }`. Declarations
hoist, so a helper used by an action above it still resolves, which is what lets the file read in
declaration order — the service's shape first, its plumbing last. A `const` arrow forces the opposite: every
utility has to appear before its first use, so the file opens with a wall of helpers and the actions are
buried.

This applies to anything outside the service's context — pure conversions, formatting, guards. Anything that
needs `apiRequest`, `config` or `files` belongs in `helpers` instead.

These two props are also where the handler **types** come from. `createExtension` resolves the service's
shape once, and every registrar carries it into every handler:

| Declared here | Becomes | Typed as |
|---|---|---|
| `configItems` | `config` in every handler (and in `initContext`) | `z.infer` of the schema |
| `initContext` | `context` in every handler | its return type |
| `helpers` | `helpers` in every handler | its return type |
| `apiRequest` | `apiRequest` in every handler | your own signature, minus the `context` / `logger` the runtime injects |

So the object `initContext` returns is the one every handler sees — widen it there, not at the call site:

```js
const ext = Flowrunner.createExtension({
  configItems: z.object({ apiKey: z.string() }),
  initContext: ({ config }) => ({ base: `https://api.svc.io/${config.apiKey}` }),
})

ext.addAction({
  // …
  execute: ({ context, config }) => {
    context.base    // string
    config.apiKey   // string
    context.typo    // ✗ context only carries what initContext returned
  },
})
```

The full per-handler context bag, and how each key is inferred, is documented in
[07-actions.md](07-actions.md#inferred-types).
