Skip to content

Commit f419dd1

Browse files
Publish bridge adapter reference journeys
1 parent 66f87f8 commit f419dd1

5 files changed

Lines changed: 267 additions & 0 deletions

File tree

app/Support/BridgeAdapterOutcomeContract.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,85 @@ public static function manifest(): array
128128
'target' => ['task_queue', 'handler'],
129129
],
130130
],
131+
'reference_journeys' => [
132+
'incident_webhook_signals_workflow' => [
133+
'pattern' => 'webhook_receiver',
134+
'operator_story' => 'An incident tool posts one provider event that signals an existing remediation workflow exactly once.',
135+
'request' => [
136+
'method' => 'POST',
137+
'path_template' => '/api/bridge-adapters/webhook/{adapter}',
138+
'adapter_example' => 'pagerduty',
139+
'action' => 'signal_workflow',
140+
'idempotency_key_source' => 'provider_event_id',
141+
'target' => [
142+
'workflow_id' => 'required existing workflow id',
143+
'signal_name' => 'required signal name',
144+
],
145+
'input' => 'optional JSON array or object carried through the configured payload envelope',
146+
],
147+
'expected_outcomes' => [
148+
'first_delivery' => [
149+
'http_status' => 202,
150+
'outcome' => 'accepted',
151+
'reason' => null,
152+
],
153+
'redelivery' => [
154+
'http_status' => 200,
155+
'outcome' => 'duplicate',
156+
'control_plane_outcome' => 'deduped_existing_command',
157+
],
158+
'missing_workflow' => [
159+
'http_status' => 422,
160+
'outcome' => 'rejected',
161+
'reason' => 'unknown_target',
162+
],
163+
],
164+
'visibility' => [
165+
'redacted_target_fields' => ['workflow_id', 'signal_name'],
166+
'command_context_metadata' => ['adapter', 'action', 'idempotency_key', 'request_id', 'signal_name'],
167+
],
168+
],
169+
'commerce_event_starts_workflow' => [
170+
'pattern' => 'webhook_receiver',
171+
'operator_story' => 'A commerce integration receives an order event and starts one durable workflow keyed by the provider event.',
172+
'request' => [
173+
'method' => 'POST',
174+
'path_template' => '/api/bridge-adapters/webhook/{adapter}',
175+
'adapter_example' => 'stripe',
176+
'action' => 'start_workflow',
177+
'idempotency_key_source' => 'provider_event_id',
178+
'target' => [
179+
'workflow_type' => 'required configured workflow type',
180+
'workflow_id' => 'optional explicit workflow id',
181+
'task_queue' => 'optional task queue override',
182+
'business_key' => 'optional user-visible dedupe or lookup key',
183+
'duplicate_policy' => ['reject_duplicate', 'use_existing'],
184+
],
185+
],
186+
'expected_outcomes' => [
187+
'first_delivery' => [
188+
'http_status' => 202,
189+
'outcome' => 'accepted',
190+
'control_plane_outcome' => 'started_new',
191+
],
192+
'redelivery' => [
193+
'http_status' => 200,
194+
'outcome' => 'duplicate',
195+
'reason' => 'duplicate_start',
196+
'control_plane_outcome' => 'returned_existing_active',
197+
],
198+
'unconfigured_workflow_type' => [
199+
'http_status' => 422,
200+
'outcome' => 'rejected',
201+
'reason' => 'unknown_target',
202+
],
203+
],
204+
'visibility' => [
205+
'redacted_target_fields' => ['workflow_id', 'workflow_type', 'task_queue', 'business_key'],
206+
'command_context_metadata' => ['adapter', 'action', 'idempotency_key'],
207+
],
208+
],
209+
],
131210
];
132211
}
133212
}

docs/contracts/bridge-adapters.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Bridge Adapter Outcome Contract
2+
3+
Bridge adapters are bounded ingress or handoff surfaces. They translate one
4+
authenticated integration event into a workflow start, signal, update, or
5+
bounded external-task handoff. They do not replay workflows, interpret event
6+
history, or own workflow state transitions.
7+
8+
The authoritative machine-readable contract is published from
9+
`GET /api/cluster/info` at `bridge_adapter_outcome_contract`:
10+
11+
- `schema: durable-workflow.v2.bridge-adapter-outcome.contract`
12+
- `version: 1`
13+
- `boundary`
14+
- `patterns`
15+
- `idempotency`
16+
- `visibility`
17+
- `outcomes`
18+
- `rejection_reasons`
19+
- `reference_journeys`
20+
21+
## Webhook Receiver
22+
23+
The first runtime bridge endpoint is:
24+
25+
```text
26+
POST /api/bridge-adapters/webhook/{adapter}
27+
```
28+
29+
The route is protected by the same control-plane version, auth, and namespace
30+
middleware as other API routes. `{adapter}` is an operator-visible adapter key
31+
made of letters, numbers, `.`, `_`, `:`, or `-`.
32+
33+
Every request must include:
34+
35+
```json
36+
{
37+
"action": "start_workflow | signal_workflow | update_workflow",
38+
"idempotency_key": "provider-event-id",
39+
"target": {},
40+
"input": {},
41+
"correlation": {}
42+
}
43+
```
44+
45+
`input` and `correlation` are optional. The response never echoes raw provider
46+
payloads, authorization material, signatures, tokens, or secrets.
47+
48+
## Reference Journey: Incident Event Signals A Workflow
49+
50+
Use this journey when an incident, alerting, or ticketing system needs to wake
51+
an existing remediation workflow.
52+
53+
```json
54+
{
55+
"action": "signal_workflow",
56+
"idempotency_key": "pagerduty-event-3003",
57+
"target": {
58+
"workflow_id": "wf-remediation-42",
59+
"signal_name": "incident_escalated"
60+
},
61+
"input": {
62+
"severity": "critical",
63+
"service": "checkout"
64+
},
65+
"correlation": {
66+
"provider": "pagerduty",
67+
"event_type": "incident.triggered"
68+
}
69+
}
70+
```
71+
72+
Expected outcomes:
73+
74+
- first delivery: HTTP 202, `outcome: accepted`
75+
- redelivery with the same adapter, idempotency key, workflow id, and signal
76+
name: HTTP 200, `outcome: duplicate`,
77+
`control_plane_outcome: deduped_existing_command`
78+
- missing workflow: HTTP 422, `outcome: rejected`, `reason: unknown_target`
79+
- malformed target: HTTP 422, `outcome: rejected`, `reason: malformed_payload`
80+
81+
The accepted command context records `adapter`, `action`, `idempotency_key`,
82+
`request_id`, and `signal_name`.
83+
84+
## Reference Journey: Commerce Event Starts A Workflow
85+
86+
Use this journey when a commerce or SaaS integration receives a provider event
87+
that should start one durable workflow.
88+
89+
```json
90+
{
91+
"action": "start_workflow",
92+
"idempotency_key": "stripe-event-1001",
93+
"target": {
94+
"workflow_type": "orders.fulfillment",
95+
"task_queue": "external-workflows",
96+
"business_key": "order-1001",
97+
"duplicate_policy": "use_existing"
98+
},
99+
"input": {
100+
"order_id": "order-1001"
101+
},
102+
"correlation": {
103+
"provider": "stripe",
104+
"event_type": "checkout.session.completed"
105+
}
106+
}
107+
```
108+
109+
Expected outcomes:
110+
111+
- first delivery: HTTP 202, `outcome: accepted`,
112+
`control_plane_outcome: started_new`
113+
- redelivery while the workflow is still active: HTTP 200,
114+
`outcome: duplicate`, `reason: duplicate_start`,
115+
`control_plane_outcome: returned_existing_active`
116+
- unconfigured workflow type: HTTP 422, `outcome: rejected`,
117+
`reason: unknown_target`
118+
- unsupported action: HTTP 422, `outcome: rejected`,
119+
`reason: unsupported_action`
120+
121+
When no explicit `workflow_id` is supplied, the server derives a stable
122+
`bridge-{adapter}-{hash}` workflow id from the adapter and idempotency key.
123+
124+
## Outcome Shape
125+
126+
All bridge responses include the contract identity:
127+
128+
```json
129+
{
130+
"schema": "durable-workflow.v2.bridge-adapter-outcome.contract",
131+
"version": 1,
132+
"adapter": "stripe",
133+
"action": "start_workflow",
134+
"accepted": true,
135+
"outcome": "accepted",
136+
"idempotency_key": "stripe-event-1001",
137+
"target": {
138+
"workflow_type": "orders.fulfillment",
139+
"task_queue": "external-workflows",
140+
"business_key": "order-1001"
141+
},
142+
"workflow_id": "bridge-stripe-...",
143+
"run_id": "..."
144+
}
145+
```
146+
147+
`target` is a redacted, operator-safe target summary. It may include workflow
148+
id, workflow type, signal name, update name, task queue, and business key. It
149+
must not include raw provider payloads or credential material.

docs/contracts/external-execution-surface.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ TLS, profile, and environment inputs deterministically.
5757

5858
Stable adjacent contract docs live in:
5959

60+
- `docs/contracts/bridge-adapters.md`
6061
- `docs/contracts/external-task-input.md`
6162
- `docs/contracts/external-task-result.md`

tests/Feature/ClusterInfoTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,18 @@ public function test_it_publishes_bridge_adapter_outcome_contract_manifest(): vo
406406
->assertJsonPath('bridge_adapter_outcome_contract.idempotency.required', true)
407407
->assertJsonPath('bridge_adapter_outcome_contract.outcomes.accepted.http_status', 202)
408408
->assertJsonPath('bridge_adapter_outcome_contract.rejection_reasons.0', 'unknown_target')
409+
->assertJsonPath(
410+
'bridge_adapter_outcome_contract.reference_journeys.incident_webhook_signals_workflow.request.action',
411+
'signal_workflow',
412+
)
413+
->assertJsonPath(
414+
'bridge_adapter_outcome_contract.reference_journeys.incident_webhook_signals_workflow.expected_outcomes.redelivery.control_plane_outcome',
415+
'deduped_existing_command',
416+
)
417+
->assertJsonPath(
418+
'bridge_adapter_outcome_contract.reference_journeys.commerce_event_starts_workflow.expected_outcomes.redelivery.reason',
419+
'duplicate_start',
420+
)
409421
->assertJsonPath('capabilities.bridge_adapter_outcome_contract', true);
410422
}
411423

tests/Unit/BridgeAdapterOutcomeContractTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public function test_manifest_defines_bounded_bridge_patterns_and_named_outcomes
2525
$this->assertArrayHasKey('duplicate', $manifest['outcomes']);
2626
$this->assertContains('unknown_target', $manifest['rejection_reasons']);
2727
$this->assertContains('unsupported_routing', $manifest['rejection_reasons']);
28+
$this->assertArrayHasKey('incident_webhook_signals_workflow', $manifest['reference_journeys']);
29+
$this->assertArrayHasKey('commerce_event_starts_workflow', $manifest['reference_journeys']);
2830
}
2931

3032
public function test_manifest_requires_redacted_visibility_fields(): void
@@ -38,4 +40,28 @@ public function test_manifest_requires_redacted_visibility_fields(): void
3840
$this->assertContains('raw_payload', $manifest['visibility']['redaction']['never_echo']);
3941
$this->assertContains('credential_ref', $manifest['visibility']['redaction']['safe_references']);
4042
}
43+
44+
public function test_reference_journeys_pin_request_and_outcome_shapes(): void
45+
{
46+
$manifest = BridgeAdapterOutcomeContract::manifest();
47+
48+
$incident = $manifest['reference_journeys']['incident_webhook_signals_workflow'];
49+
50+
$this->assertSame('webhook_receiver', $incident['pattern']);
51+
$this->assertSame('/api/bridge-adapters/webhook/{adapter}', $incident['request']['path_template']);
52+
$this->assertSame('signal_workflow', $incident['request']['action']);
53+
$this->assertSame('provider_event_id', $incident['request']['idempotency_key_source']);
54+
$this->assertSame(202, $incident['expected_outcomes']['first_delivery']['http_status']);
55+
$this->assertSame('deduped_existing_command', $incident['expected_outcomes']['redelivery']['control_plane_outcome']);
56+
$this->assertSame('unknown_target', $incident['expected_outcomes']['missing_workflow']['reason']);
57+
$this->assertContains('request_id', $incident['visibility']['command_context_metadata']);
58+
59+
$commerce = $manifest['reference_journeys']['commerce_event_starts_workflow'];
60+
61+
$this->assertSame('start_workflow', $commerce['request']['action']);
62+
$this->assertContains('use_existing', $commerce['request']['target']['duplicate_policy']);
63+
$this->assertSame('started_new', $commerce['expected_outcomes']['first_delivery']['control_plane_outcome']);
64+
$this->assertSame('duplicate_start', $commerce['expected_outcomes']['redelivery']['reason']);
65+
$this->assertContains('business_key', $commerce['visibility']['redacted_target_fields']);
66+
}
4167
}

0 commit comments

Comments
 (0)