Poll a REST API from a job
Updated
A nightly or periodic pull from a shop or an operator. The job is a cron tick. The service names a channel, waits for the body, and upserts. Create a job is the trio; this page is the REST half.
Note
Never
HttpClientfrom a script. Name the channel. The host owns URL, credential, signature, and SSRF — What a script can do, Send an outbound call.
How to#
1. The job is thin. Three files in Jobs/<Name>/, inherit
TaskScriptBase, executeSingle: true. Resolve a service, call it, log.
There is no Chain of Command on a job. On import, jobs arrive
disabled even if isActive: true in the file. A test drives the run
with Db.RunJobAsync(jobMetaId) — Create a job.
RecalcAbcClassifications is that shape (nightly ABC). It does not
talk HTTP. Jobs that do pull: PullUaBankStatements (IUaBank,
channel pb-statements) and SyncFromOneC (IOneCSync, channel
onec-odata). ApplyFatooraOutcomes drains the local outbox
(IOutboundGateway.TakeCompletedAsync); it is not a REST poll.
2. Pick the door. A pull needs the body now so you can upsert.
That is IOutboundCall.SendAsync, not the outbox. IOutboundGateway is
for a document you must not lose after posting —
Enqueue an outbound call when a document posts.
The table: Integrate with a third-party REST API.
Example only — job PullShopItems, channel shop-items-pull, and
IShopCatalog are invented; no model ships them today.
public class PullShopItemsTask : TaskScriptBase
{
public override async Task ExecuteAsync(TaskContext context)
{
var n = await ScriptServices.Get<IShopCatalog>().PullAsync();
context.Log("upserted=" + n);
}
}
The service names the channel.
var result = await ScriptServices.Get<IOutboundCall>().SendAsync(
"shop-items-pull", payload: "", idempotencyKey: "shop-items:nightly");
if (!result.Ok) return 0;
// result.Body — parse, then upsert
Both doors take a channel name, not a URL. GET / HEAD go with no
body. A per-request path belongs in X-ZuloOne-RelativePath, not in
HttpClient. OutboundCallResult.Ok is HTTP 2xx; a transport failure
returns Error and does not throw.
3. Upsert by business key. A second tick must not insert a second
row. Find, then SaveRecordAsync (insert or update) — not
NewRecordAsync every run.
var items = ScriptServices.Get<IDictionaryManager<Item>>();
var existing = await items.GetRecordsAsync("Article = 'SKU-1'", take: 1);
var row = existing.Count > 0 ? existing[0] : await items.NewRecordAsync();
row.Article = "SKU-1";
row.Name = nameFromShop;
await items.SaveRecordAsync(row);
Article is a real field on Item. When the shop row is already a
public projection, upsert through IDataEntityManager
(QueryAsync / InsertAsync / UpdateAsync by collection name) —
Create a data entity. The
idempotencyKey on IOutboundCall reaches the signer; it is not an
outbox unique index. The upsert is the guard.
4. Channel in your model, bump modelVersion, and
POST /api/services/rebuild for a new channel —
Send an outbound call.
Warning
A successful call is not a receipt.
InDoubtis an outbox status: do not retry that submit. A pull that failed to parse just runs again next tick; the upsert keeps it idempotent.Cover everything before the send and everything after the response. An integration test cannot cover the HTTP exchange. Drive the job with
Db.RunJobAsync; feed the service a sample body for the upsert — Write an integration test.
What's not there yet#
The platform does not accept an inbound webhook. CompletionHandler is
reserved and unread. A web service you publish
is RPC they invoke, not a callback that completes this pull.