Integrations / Server-side TypeScript
LangGraph
Record a graph decision as its own task.
Use a Functional API task from an existing entrypoint after your decision step. Await the ADR acknowledgement before the workflow reports that its evidence was recorded.
import { task } from '@langchain/langgraph';
import { recordDecision, type DecisionMetadata, type RecourseEnv } from './record-decision';
// Call this task from your existing Functional API entrypoint after its decision step.
export function decisionReceiptTask(env: RecourseEnv) {
return task({ name: 'record-decision' }, async (meta: DecisionMetadata) => {
return await recordDecision(env, meta);
});
}Received: request acknowledged. Acknowledged is not durable. Durable: committed to the configured persistence layer. Sealed: included in a signed root with a receipt. The API wire values remain accepted, durable and sealed. Searchable-through is a separate index coverage watermark: not available in this preview.
Development signer means integrity under a published dev key and no independently trusted time.
The task takes only public decision metadata. Preserve eventId and occurredAt in your workflow state so replaying the same decision uses the original idempotency key and payload. A completed task returns the ADR response. Inspect its state before treating the record as Durable or Sealed.
Choose what enters the record
All five SDK examples use the same recordDecision helper. Supply RECOURSE_URL, RECOURSE_TENANT_ID and RECOURSE_API_KEY from the server environment. It sends POST /v1/adr with Bearer authentication, tenant scope and the event’s idempotency key.
Public metadata uses opaque subject, actor and action tokens; token syntax alone does not make an identifier nonidentifying. The helper always sends private_fields: {}.
Read the shared transport and retry handling
/** Server-only ADR transport. Documentation example; no top-level network call. */
export type RecourseEnv = {
RECOURSE_URL: string;
RECOURSE_TENANT_ID: string;
RECOURSE_API_KEY: string;
};
export type DecisionMetadata = {
eventId: string;
occurredAt: string;
subjectId: string;
actorId: string;
action: string;
outcome: 'allow' | 'deny' | 'defer' | 'error' | 'abstain' | 'other';
};
export class RecourseHTTPError extends Error {
constructor(readonly status: number, readonly retryAfterSeconds?: number) {
super(`Recourse request failed (HTTP ${status})`);
this.name = 'RecourseHTTPError';
}
}
const token = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const outcomes = new Set(['allow', 'deny', 'defer', 'error', 'abstain', 'other']);
export function decisionPayload(meta: DecisionMetadata) {
if (![meta.eventId, meta.subjectId, meta.actorId, meta.action].every(v => typeof v === 'string' && token.test(v)) ||
!outcomes.has(meta.outcome) ||
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(meta.occurredAt) ||
!Number.isFinite(Date.parse(meta.occurredAt)) || new Date(meta.occurredAt).toISOString() !== meta.occurredAt) {
throw new Error('Invalid public decision metadata');
}
return {
schema_version: '1.0' as const,
occurred_at: meta.occurredAt,
subject_id: meta.subjectId,
actor: { type: 'agent' as const, id: meta.actorId },
decision: { action: meta.action, outcome: meta.outcome },
private_fields: {},
};
}
export async function recordDecision(
env: RecourseEnv,
meta: DecisionMetadata,
transport: typeof fetch = fetch,
pause: (ms: number) => Promise<void> = ms => new Promise(resolve => setTimeout(resolve, ms)),
): Promise<{ adrId: string; ack: 'accepted' | 'durable' | 'sealed' }> {
let base: URL;
try { base = new URL(env.RECOURSE_URL); } catch { throw new Error('Invalid Recourse environment'); }
if ((base.protocol !== 'https:' && !(base.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(base.hostname))) ||
base.username || base.password || base.search || base.hash ||
typeof env.RECOURSE_TENANT_ID !== 'string' || !token.test(env.RECOURSE_TENANT_ID) ||
typeof env.RECOURSE_API_KEY !== 'string' || !env.RECOURSE_API_KEY || /[\r\n]/.test(env.RECOURSE_API_KEY)) {
throw new Error('Invalid Recourse environment');
}
// Keep the same event ID, timestamp and serialized payload across all retries.
const body = JSON.stringify(decisionPayload(meta));
for (let attempt = 0; attempt < 3; attempt++) {
let response: Response;
try {
response = await transport(new URL('/v1/adr', base), {
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10_000),
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${env.RECOURSE_API_KEY}`,
'X-Tenant-ID': env.RECOURSE_TENANT_ID,
'Idempotency-Key': meta.eventId,
},
body,
});
} catch {
if (attempt === 2) throw new Error('Recourse transport unavailable; retain this event for retry');
await pause(250 * 2 ** attempt);
continue;
}
const retryHeader = response.headers.get('Retry-After');
const retryAfter = retryHeader && /^\d+$/.test(retryHeader) ? Number(retryHeader) : undefined;
if (!response.ok) {
await response.body?.cancel();
// Quota, auth, conflict and schema errors require caller action; do not retry them here.
if (![408, 500, 502, 503, 504].includes(response.status) || attempt === 2 || (retryAfter ?? 0) > 30) {
throw new RecourseHTTPError(response.status, retryAfter);
}
await pause(Math.max(250 * 2 ** attempt, (retryAfter ?? 0) * 1_000));
continue;
}
let result: unknown;
try { result = await response.json(); } catch { throw new Error('Invalid Recourse acknowledgement; retain this event for retry'); }
const record = (result as { records?: { adr_id?: unknown; ack?: unknown }[] })?.records?.[0];
if (!record || typeof record.adr_id !== 'string' || !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/.test(record.adr_id) ||
!['accepted', 'durable', 'sealed'].includes(String(record.ack))) {
throw new Error('Invalid Recourse acknowledgement; retain this event for retry');
}
return { adrId: record.adr_id, ack: record.ack as 'accepted' | 'durable' | 'sealed' };
}
throw new Error('Recourse transport unavailable; retain this event for retry');
}Keep the decision through retries
Keep the original metadata in a durable application outbox. Retry the ADR write after a transport failure without rerunning a completed provider action. The helper makes up to three attempts for transient errors, including the initial request; authentication, schema, conflict and quota failures return to the caller. It never logs response bodies or credentials.
After the request is acknowledged, poll the record status through the normal minute boundary. Fetch a sealed receipt and verify it with an explicit trust anchor. A Durable response means persistence, while Sealed means a receipt is available for verification.
R0 examples are source-checked and syntax-checked, with the shared HTTP transport tested using local mocks. Provider packages are not installed or executed here; full SDK type compatibility and live provider integration remain unqualified. The ADR schema is provisional pending the unavailable D-002 source.
API references checked 5 September 2026: LangGraph JavaScript quickstart. Documentation examples only; no provider call or live Collector export was executed.