> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bijection.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Integration Probes

> Check that an installed source works, and verify the cleanup of anything the check created

A **probe** is a bounded check declared with an integration and run against one
installed source. It exercises the source through the integration's own
read-only requests, or through temporary records that the run creates and then
removes. Each run reports two results separately: whether its checks passed,
and whether its cleanup was verified.

A probe never promises that nothing happened: reads can use provider quota and
appear in audit logs, and deleting a record can't undo the provider's audit
entries, notifications or downstream reactions. A run promises a checked result
and explicit evidence about every record it created.

## Declaring a probe

Declare probes in the integration's `probes` factory. It receives the
integration's own `requests` and `commands`; only `GET` and `HEAD` requests that
read from the provider are offered as `requests`, and never an incremental sync
request, which reads with the source's own sync tokens. List the handles a probe
uses in `reads` and `writes`. This read-only probe checks that a partner
connection still authenticates:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
probes: ({ requests }) => ({
  /** Whether the connection still authenticates. Read-only: one identity
   * read, which changes nothing. The supplier export is not offered: its
   * requests read with the source's own sync tokens. */
  connection: defineProbe({
    reads: [requests.identity],
    async run(p) {
      const identity = await p.read(requests.identity);
      await p.check("authenticated", identity.status, 200);
    },
  }),
}),
```

The context `p` offers:

* `p.read(request, parameters?)` sends a declared request and resolves to its
  HTTP outcome, with `status` and `body`.
* `p.check(label, actual, expected)` compares two values exactly. A different
  value fails the run at that check.
* `p.create(resource, args, { remove_args }?)` creates a declared temporary
  record. Its removal arguments are fixed when it is created.
* `p.write(command, resource, args)` sends a declared command to a record this
  run created.
* `p.waitFor(resource, expected, { within_ms }?)` waits until the source
  observes the record with those field values; `{}` waits for it to exist.
* `p.run_id` identifies the run.

Every call is a durable step, named from its kind (`read.identity.1`,
`create.account.1`) or by the check's label. Await each call before making the
next. A run is replayed from its journal, so `run` must make the same calls in
the same order every time, and it has no direct network access, clock or
randomness. Declare inputs with `args`, as for an operation; the deployment
checks them when a run starts.

### Temporary records

A probe that declares `resources` may create records. Each resource names the
command that creates it and the command that removes it; both must target the
same collection, and that collection must declare `key_reuse: "never"`, meaning
the provider never gives a deleted record's key to another record.

```ts bijection/crm.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineIntegration, defineProbe } from "bijection/server";
import { v } from "bijection/values";

export const crm = defineIntegration({
  collections: {
    accounts: {
      key_reuse: "never",
      schema: { name: v.string() },
      protocol: { … },
      sync: { … },
    },
  },
  commands: {
    createAccount: { target: "accounts", args: { name: v.string() }, … },
    removeAccount: { target: "accounts", args: {}, … },
  },
  probes: ({ commands }) => ({
    /** Creates an account, waits until a sync sees it, then removes it. */
    accountRoundTrip: defineProbe({
      args: { name: v.string() },
      resources: {
        account: {
          create: commands.createAccount,
          remove: commands.removeAccount,
        },
      },
      async run(p, args) {
        const account = await p.create("account", { name: args.name });
        await p.waitFor(account, { name: args.name }, { within_ms: 30_000 });
      },
    }),
  }),
  // requests, identify …
});
```

There is no explicit remove: once the exercise ends, the run removes every
record it created, whatever the result. The provider's own create response
identifies the record, so a probe can't point cleanup at anything else. Cleanup
is verified only when the source is observed again without the record; an
unknown create or delete outcome never counts as removed. Only a signed-in user
can start a probe that creates records, and every command it sends must pass
that command's ordinary governing rule.

A run allows at most 64 steps, eight records, a 15-minute exercise and a
24-hour cleanup window. The defaults are 32 steps, two minutes and one hour;
change them with `limits`.

## Results

A run's summary reports its assessment and its cleanup separately, and a phase
derived from both:

| Field | Values |
| - | - |
| `assessment` | `pending`, `passed`, `failed` (a check saw a different value; `step` names it), `inconclusive` (an `issue` ended the exercise) or `cancelled`. Once decided, it never changes. |
| `cleanup` | `not_required`, `pending`, `verified`, `blocked` with an `issue`, or `abandoned` with the issue that blocked it. |
| `phase` | `running`, `settling` (assessment decided, cleanup in progress), `blocked` (cleanup is blocked) or `closed`. |

`closed` doesn't mean passed: read the assessment and the cleanup. An issue is
a typed cause such as `binding_changed`, `source_unavailable`,
`command_refused` or `code_error` (the probe's own code threw). Infrastructure
failures are retried and never become issues. The full report adds each step
and record with its status, and the controls you may apply now. It never
contains provider payloads, credentials or inputs.

## Running probes

From the CLI, list a source's probes, review one's plan, run it and inspect
the result:

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection integration probe list <source>
bijection integration probe plan <source> connection
bijection integration probe run <source> connection --wait
bijection integration probe show <run>
```

`plan` prints what the probe may do on that source and its fingerprint;
`--plan <fingerprint>` makes `run` start only under the plan you reviewed, and
`--args` passes inputs as a JSON object. `--wait` follows the run for up to
five minutes; disconnecting doesn't cancel it. `history <source> [probe]` pages
through past runs, including probes whose declaration was removed. A request
whose response was lost stays in a local journal: `pending <source>` lists
those, `retry <run>` resends one exactly and `discard <run>` forgets it. See the
[CLI reference](/cli/reference/integration#bijection-integration-probe).

From your code, use the `probes` references generated in
`_generated/integrations`:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { BijectionHttpClient } from "bijection/browser";
import { fileRequestStore } from "bijection/node";
import { probes } from "../bijection/_generated/integrations";

const client = new BijectionHttpClient(url, {
  auth: token,
  requests: fileRequestStore("/var/lib/my-app/requests"),
});
const start = client.probes.prepare(probes.partners.connection, {
  source_id,
  args: {},
});
const run = await start.start();
const report = await run.wait();
console.log(report.summary.assessment, report.summary.cleanup);
```

`start()` admits the current plan, or only the one you pass as
`expected_plan`. After a lost response, start again with the same `run_id`,
even from another process. `client.probes.attach(run_id)` observes any run.
`wait()` returns when the run is closed or its cleanup is blocked; its timeout
throws `ProbeWaitTimeout` and never cancels the run. In a browser, the local
journal uses encrypted browser storage; in Node, pass `fileRequestStore`.
Refusals are `ConnectionRequestError`s with a `code`, such as
`ProbeRunUnfinished`: a new run of a probe on a source waits until the last one
is closed.

In a React app rendered under `BijectionProvider`, `IntegrationProbes` lists a
source's probes and runs, starts runs, shows each step and record, and offers
the controls the selected run allows:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { IntegrationProbes } from "bijection/react";

export function SourceChecks({ source_id }: { source_id: string }) {
  return <IntegrationProbes source_id={source_id} />;
}
```

`disabled` turns off starting and controlling runs; inspection still works.
For your own interface, `useProbes({ source_id })` returns the same state and a
`controller`.

## Controls

| Control | Effect | Who may use it |
| - | - | - |
| `cancel` | Ends the exercise; cleanup continues. | The user who started the run, or an operator with write permission |
| `resume_cleanup` | Opens another bounded window for blocked cleanup. The assessment is never restarted. | Only the user who started the run, when every blocking issue can be resumed |
| `abandon_cleanup` | Stops blocked cleanup and closes the run. The provider may still hold the remaining records. | The user who started the run, or an operator with write permission |

Apply them with `cancel`, `resume-cleanup` and `abandon-cleanup <run>` in the
CLI, or `cancel()`, `resumeCleanup()` and `abandonCleanup()` on a run. A control
applies to the report you reviewed: if the run changed meanwhile, it's refused
with `ProbeRevisionChanged`, and retrying a control after a lost response
returns its original result.

A signed-in user sees and controls only the runs they started, and only while
they can still use the source's connection. Deployment operators see every run
(`--operator` in the CLI); with write permission they can also start read-only
probes. Probes can't be started or controlled from a branch.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.