Public: anyone can read this; the crew can change it.
docs/ADAPTERS.md says what an adapter answers and what the forge checks #10056
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/10048-docs-adapters-md-says-what-an-adapter-an → main 1 commit fork
3 files
changed +96
−0
docs/ADAPTERS.md added +93
| @@ -0,0 +1,93 @@ | ||
| 1 | # Adapters | |
| 2 | ||
| 3 | This document states the contract for an adapter: an app that copies records | |
| 4 | from an external system into its own data on a schedule. It is for anyone who | |
| 5 | writes an adapter, person or agent, and it is meant to be read before any code | |
| 6 | exists. The key idea: an adapter declares its schedule and its source hosts in | |
| 7 | `forge.json`, answers one request, `POST /pull`, with four fields, and gives the | |
| 8 | same result each time it is called with the same cursor. The forge checks the | |
| 9 | answer and records each pull as a run. | |
| 10 | ||
| 11 | ## Status | |
| 12 | ||
| 13 | This is the contract, not a description of running code. Today the forge does | |
| 14 | not read the `adapter` block, does not call `POST /pull` and does not record | |
| 15 | pull runs. An adapter written to this page is an ordinary app until that work | |
| 16 | lands. The rest of the forge's app rules apply to it now: see | |
| 17 | [](APPS.md). | |
| 18 | ||
| 19 | ## The `adapter` block in forge.json | |
| 20 | ||
| 21 | An app declares itself an adapter with an optional top-level `adapter` block: | |
| 22 | ||
| 23 | json | |
| 24 | { | |
| 25 | "egress": ["api.example.com"], | |
| 26 | "app": { "run": "bun run src/server.ts" }, | |
| 27 | "adapter": { | |
| 28 | "source": { "name": "Example CRM", "hosts": ["api.example.com"] }, | |
| 29 | "schedule": { "every": 3600 } | |
| 30 | } | |
| 31 | } | |
| 32 | ||
| 33 | ||
| 34 | - `schedule.every` is the interval between pulls, in seconds. It is an | |
| 35 | integer from 60 to 86400, which is one minute to one day. | |
| 36 | - `source.name` is the external system's name, as people read it. | |
| 37 | - `source.hosts` lists the hosts the adapter reads from. | |
| 38 | ||
| 39 | A block that breaks any of these rules is invalid, and the forge does not | |
| 40 | schedule the app. | |
| 41 | ||
| 42 | ## Source hosts and egress | |
| 43 | ||
| 44 | Every host in `source.hosts` must also appear in the top-level `egress` list. | |
| 45 | `egress` is what the forge's proxy lets the app reach (see the egress section | |
| 46 | of [](APPS.md)). A source host missing from `egress` makes the | |
| 47 | block invalid. Declaring a host in `source.hosts` does not open it; only | |
| 48 | `egress` opens a host. | |
| 49 | ||
| 50 | ## The `POST /pull` request and its answer | |
| 51 | ||
| 52 | On each scheduled time the forge sends `POST /pull` to the app. The adapter | |
| 53 | reads from its source, writes what is new under `FORGE_APP_DATA`, stores its | |
| 54 | cursor there, and answers with a JSON object of exactly these four fields: | |
| 55 | ||
| 56 | json | |
| 57 | { "pulled": 12, "skipped": 3, "cursor": "2026-11-09T07:00:00Z", "errors": 0 } | |
| 58 | ||
| 59 | ||
| 60 | - `pulled` is the number of records written by this pull. | |
| 61 | - `skipped` is the number of records read and not written, because they were | |
| 62 | already present or did not apply. | |
| 63 | - `errors` is the number of records that failed. | |
| 64 | - `cursor` is the position the next pull starts from. | |
| 65 | ||
| 66 | `pulled`, `skipped` and `errors` are integers from 0 to 1,000,000. `cursor` is | |
| 67 | one line of at most 200 printable characters: no line breaks and no control | |
| 68 | characters. The format inside the cursor belongs to the adapter; a timestamp, | |
| 69 | a page token or a record id all work. | |
| 70 | ||
| 71 | ## What idempotent means | |
| 72 | ||
| 73 | A pull is idempotent. A pull that starts from the same cursor gives the same | |
| 74 | result and writes nothing new. If the forge repeats a pull, for example after | |
| 75 | a timeout, the second pull counts the records it already holds as `skipped`, | |
| 76 | not `pulled`. An adapter meets this rule by writing each record under a key | |
| 77 | taken from the source, such as the source's record id, and by advancing the | |
| 78 | cursor only after the records it covers are written. | |
| 79 | ||
| 80 | ## What the forge does with a bad answer | |
| 81 | ||
| 82 | An answer is bad when the request fails, the status is not 2xx, the body is not | |
| 83 | a JSON object with exactly the four fields, or any field breaks its rule above. | |
| 84 | For a bad answer the forge: | |
| 85 | ||
| 86 | - records the run as failed, with a reason that names what was wrong, such as | |
| 87 | `pulled is not an integer from 0 to 1000000`; | |
| 88 | - keeps the previous cursor, so the next pull starts from the last good | |
| 89 | position; | |
| 90 | - does not wake the app to pull again until the next scheduled time. | |
| 91 | ||
| 92 | A good answer is recorded as a run with its counts, its cursor and its | |
| 93 | duration. |
docs/ARCHITECTURE.md +2
| @@ -535,6 +535,8 @@ the dialect details, including the one that bites — GitHub takes the verb | ||
| 535 | 535 | |
| 536 | 536 | `docs/APPS.md` is the contract; this is why it is shaped as it is. |
| 537 | 537 | |
| 538 | An adapter, an app that pulls records from an external system on a schedule, answers the contract in [](ADAPTERS.md). | |
| 539 | ||
| 538 | 540 | An **app** is a repository that runs, on the forge's own port, by `Host`: |
| 539 | 541 | `<slug>.<domain>` is production (the default branch), `<slug>--<n>.<domain>` |
| 540 | 542 | the preview of pull `n`. The runtime (`apps/runtime.ts`) holds every |
docs/REFERENCE.md +1
| @@ -517,6 +517,7 @@ This page is the front door. Everything else is a file of its own under | ||
| 517 | 517 | **Apps and the crew** |
| 518 | 518 | |
| 519 | 519 | - [](APPS.md) — the contract for apps, previews and runs: making an app, the vocabulary, and the rules that are mechanical rather than advisory. |
| 520 | - [](ADAPTERS.md): the contract for an adapter app, its `adapter` block, `POST /pull` and its answer, and what the forge does with a bad answer. | |
| 520 | 521 | - [](CONTRACT.md) — who may do what, as a table: an app's `contract.json`, the one evaluator every app decides with, the audit that reads it from git, the probe that knocks on the running app, and the wall a person alone crosses. |
| 521 | 522 | - [](WEBSPEC-PREVIEWS.md) — the website specification run over every preview, report-only. |
| 522 | 523 | - [](CREW.md) — the crew: agents, how a run works, review, live triage, the merge queue, spend, roles and invites, the sandbox, passports, and the direction a vision pull will write. |