Enqueue an outbound call when a document posts
Updated
After GoodsIssue or an invoice posts, send it to a WMS or an operator.
The outbox row is written in the same transaction as posting. The
host sends later. A job after the fact is the wrong door —
Poll a REST API from a job.
Note
Never
HttpClientfrom a script.IOutboundGateway.EnqueueAsyncby channel name. The host owns URL, secret, signature, retry, and SSRF — Send an outbound call, What a script can do.
How to#
1. Enqueue from the posting path. OnAfterPostAsync runs inside the
transition: movements, subtype, and audit are still uncommitted. A
thrown handler rolls the post back, and a rolled-back post sends
nothing. GetTransactions stays a synchronous movement calculation —
Write postings. Enqueue is orchestration: a service
the event (or another posting-path caller) invokes is fine; the
EnqueueAsync itself must stay on that path, not on a later job.
OnAfterPostAsync can run twice (repost, a totals driver under the same
doc id). IdempotencyKey is the guard: the unique index is
(Channel, IdempotencyKey). Same key, same payload — the existing row.
Same key, different payload — throw. Derive the key from the document,
not from the clock.
// Example only — channel `wms-ship` and IWmsShip are invented.
// Inventory.GoodsIssue has no WMS enqueue today.
await ScriptServices.Get<IOutboundGateway>().EnqueueAsync(new OutboundRequest
{
Channel = "wms-ship",
IdempotencyKey = "wms:" + document.MetaId.ToString("N"),
Payload = body,
SourceTable = "GoodsIssue",
SourceRecordId = document.MetaId,
});
The working pattern in the tax packs: OnAfterPostAsync on
SalesRealization calls ISaudiEInvoice.EnsureForInvoiceAsync /
IUaEInvoice.EnsureForInvoiceAsync, and that service enqueues
(fatoora-clearance, erpn-send). EnqueueAsync only writes a row.
OutboundSenderService delivers it outside the transaction so
DeadlockRetry cannot replay an HTTP POST.
2. Owner vs wrap. The handler in the document's own model is link 0
and does not call next. A handler on a foreign document must
declare [ExtensionOf("GoodsIssue")] and call next from every
override — Write an event handler,
Wrap with Chain of Command. Missing
[ExtensionOf] is ZOCOC004; missing next is ZOCOC001.
[ExtensionOf("GoodsIssue")]
public partial class GoodsIssueWmsEventHandler : TypedDocumentEventHandler<GoodsIssue>
{
public override async Task<EventResult> OnAfterPostAsync(
GoodsIssue header, EventContext context)
{
var prior = await next(header, context);
if (!prior.Success) return prior;
await context.GetService<IWmsShip>().EnqueueIssueAsync(header);
return EventResult.Ok();
}
}
Example only — there is no GoodsIssueWmsEventHandler in Inventory.
Work after next() sees stock and GL.
3. Channel in your model (OutboundChannels/<name>.channel.json),
bump your modelVersion in the same change, and
POST /api/services/rebuild for a new channel —
Bump the model version.
Warning
A successful send is not a receipt. Ask whether the response is the other side's decision, or "accepted for processing". Closing the document on
Succeededwhen a receipt still has to be fetched registers work the other system has not seen.
InDoubtmeans the request left and no answer came back. The document may already be registered. Do not retry that submit. Query fate withIOutboundCall; do not enqueue a second submit.
EnqueueAsyncopens its own connection. Call it from a posting event or a command — not from an after-insert that already read another table (that second connection promotes the scope to MSDTC).
Signing, four guards against a second submit, TakeCompletedAsync:
Outbound integrations. Door table:
Integrate with a third-party REST API.
What's not there yet#
The platform does not accept an inbound callback that completes the
send. CompletionHandler is reserved and unread. Drain your own outbox
for send outcomes; poll the other side when you need a receipt —
Poll a REST API from a job. An
integration test cannot cover the HTTP exchange: cover everything before
the enqueue and everything after the response; say so in the test
header.