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

# Write-back adapters

> Turn an accepted proposal into a write against a system of record using ShoulderTap's OpenMetadata and generic webhook adapters.

An **adapter** is a consumer-side helper that writes an accepted proposal into a system of record. Instead of hand-writing the logic that turns a `proposal_accepted` event into an API call, you hand the proposal to an adapter and it performs the write, returning a result you can log. Two adapters ship with ShoulderTap.

## The adapter interface

Every adapter implements a single method:

```python theme={null}
result = adapter.on_accepted(proposal)   # -> WriteResult
result.success   # bool
result.detail    # str | None — the target identifier on success, or the error
```

Call it from your webhook handler (or in-process callback) when a `proposal_accepted` event arrives.

## OpenMetadata adapter

Writes an accepted `glossary.definition` proposal into an [OpenMetadata](https://open-metadata.org/) glossary term — it PATCHes the term's description with the definition plus a provenance footer, so the catalog records who answered and when.

```python theme={null}
from shouldertap.adapters.openmetadata import OpenMetadataAdapter

adapter = OpenMetadataAdapter(
    host="https://om.example.com",
    token="...",
    entity_fqn_resolver=lambda proposal: entity_fqn_for(proposal.request_id),
)

result = adapter.on_accepted(proposal)
# result.detail == the glossary term FQN that was updated
```

<ResponseField name="host" type="string" required>
  Base URL of your OpenMetadata instance.
</ResponseField>

<ResponseField name="token" type="string" required>
  Bearer token for the OpenMetadata API.
</ResponseField>

<ResponseField name="entity_fqn_resolver" type="callable" required>
  A function mapping a proposal to the fully-qualified name of the glossary term to update. Because a `ContextProposal` doesn't carry the original request's `context`, you supply this mapping — typically keyed by `proposal.request_id`.
</ResponseField>

The provenance footer looks like `Source: Dana Kim via ShoulderTap, 2026-07-20`. The adapter rejects non-glossary kinds (`detail` contains `unsupported kind`) and a resolver that returns `None` (`detail` is `no entity FQN`). If structuring failed and `structured` is null, it falls back to the raw `answer`.

## Webhook adapter

The universal escape hatch: POSTs the accepted proposal as JSON to any URL. Use it to forward answers into a system that doesn't have a dedicated adapter yet.

```python theme={null}
from shouldertap.adapters.webhook import WebhookAdapter

adapter = WebhookAdapter("https://my-system.example.com/hook")
result = adapter.on_accepted(proposal)   # POSTs the proposal JSON
```

On a connection error or a non-2xx response, `result.success` is `False` and `result.detail` describes the failure — the adapter never raises, so a failed write-back won't crash your consumer.

## Putting it together

The `examples/openmetadata_sync/webhook_consumer.py` in the repository wires the OpenMetadata adapter behind a FastAPI webhook: it registers as a `glossary.definition` consumer, remembers which entity each request maps to, and on each `proposal_accepted` event PATCHes the definition into OpenMetadata. It's a complete, runnable template for a write-back consumer.
