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

# Publishing

> How a decision becomes a valid record in each destination: plan, preflight, publish, the enrichment loop and the idempotency key.

Publishing turns a decision into a valid record in each destination that needs one, without duplicates and without losing the thread back to the decision. This page covers the three Intent API calls involved and the guarantees they carry. Read it before you call `POST /publish`.

```mermaid theme={"system"}
flowchart LR
  P["POST /plan"] --> F["POST /preflight"]
  F -->|"ok"| U["POST /publish"]
  F -->|"422 enrichment_request"| E["POST /enrichment-request"]
  E -->|"same intent_version"| F
  U -->|"find before create"| D["Destination record with Intent ID"]
  D --> R["Receipt: idempotency_key, external_id, deep_link, replayed"]
```

## Three steps

<Steps>
  <Step title="Plan">
    `POST /plan` applies the Channel Map, the rules that say which kind of work goes to which destination, and returns the destinations this intent will be published to.

    The request names the `intent_id`; `channel` defaults to the channel on the intent's payload, and `platform` overrides the Channel Map with an explicit destination. The response lists each destination with its `platform`, `object_type`, `carrier_field` (where the Intent ID is written), `mode` (`stamp` or `link`) and `events_posture` (`push`, `poll` or `mixed`), plus a deterministic `rationale` for the choice. Scope: `intents:write`.
  </Step>

  <Step title="Preflight">
    `POST /preflight` checks the intent against one destination's own requirements: the vendor's create schema and the tenant's own mandatory fields, picklists and custom fields.

    A clean pass returns `200` with `ok: true`, the `profile` validated against and an empty `missing_fields`. A gap returns `422` with an `enrichment_request` naming the missing canonical fields, the `target_system`, the `profile` that flagged them and, per field, an optional `target_field` and `hint`. The question reaches the requester before anything is written. Scope: `intents:write`.
  </Step>

  <Step title="Publish">
    `POST /publish` renders the destination's payload from the mapping profile, checks for an existing record carrying the Intent ID, creates one only if none exists, and returns `201` with a receipt.

    The receipt carries `external_id` (the destination's own record identifier), `idempotency_key`, `deep_link` (one click to the record in the destination), `status`, `replayed` and `at`. Supply `external_id` in the request when the record already exists: composerID stamps the Intent ID onto it instead of creating a duplicate. That is required for link-mode destinations. A tenant gap found at publish time returns the same `422` as preflight. Scope: `publish`.
  </Step>
</Steps>

## Idempotency

Every publish is identified by a deterministic key:

```text Idempotency key theme={"system"}
{intent_id}-{intent_version}-{target_system}[-{object_type}]
```

The `object_type` suffix appears only when one intent produces more than one record in a destination. A repeated publish returns the original receipt with `replayed: true` and touches nothing. Bumping `intent_version` mints a new key on purpose, so a genuine amendment publishes again while retries of the old key stay inert.

<Check>
  For a given decision and version there is at most one record per destination. Where a destination honours an idempotency header the adapter sends one; where it documents none, the adapter queries the destination for a record already carrying the Intent ID before creating one.
</Check>

## The enrichment loop

Preflight gaps become questions, not failures. The `enrichment_request` is a published schema an upstream surface can render in the requester's own words. The answers return through `POST /enrichment-request` as an `answers` object keyed by canonical field, preflight runs again, and publishing proceeds.

<Info>
  Enrichment completes the same decision, so it does not bump `intent_version` and the publish key is unchanged. The intent is never half-published: the publish is withheld until every gap is filled.
</Info>

## Mapping profiles

A mapping profile per destination translates the canonical intent fields into the destination's dialect: `role_title` becomes an SAP Fieldglass job posting title, a Beeline `requestName`, a Coupa requisition line description. Preflight validates against the profile, publish renders from it, and reconciliation compares through it. Swapping a destination is a configuration change, not a rewrite.

<Warning>
  The payload a profile renders from contains only request fields. No diagnostic answer, score or ranked alternative has a path into a destination field, whatever the profile says.
</Warning>

<Info>
  All of this is implemented in the reference sandbox and held to the documented contract by a conformance pack that runs in continuous integration. The production service behind `api.composer.id` is in build; nothing here claims it is live.
</Info>

## Next steps

<Columns cols={3}>
  <Card title="API reference" icon="braces" href="/api-reference/overview">
    Every route, request body, response and scope.
  </Card>

  <Card title="Events and the timeline" icon="webhook" href="/concepts/events">
    What happens after the receipt: webhooks, reconciliation and drift.
  </Card>

  <Card title="Published schemas" icon="file-text" href="/concepts/schemas">
    EnrichmentRequest, PublishingBundle and MappingProfile at their canonical URLs.
  </Card>
</Columns>
