Contexts
Bulk Ingestion

Bulk Ingestion

Every context accepts bulk ingestion. Stream large datasets into a context by submitting them in chunks. Each chunk is one synchronous API call: the records merge into their context instances, any bound rules or flows whose inputs became satisfied execute, and the response returns the resolved state of every touched instance along with what executed.

There are no job IDs, no polling, and no separate result-retrieval step. Instances waiting on dependent data simply persist with pending status until a later chunk (or an interactive update) completes them. The same progressive semantics as single-record submissions, at volume.

Submitting Batches

POST /api/v1/contexts/batch/{context-slug}

The body is a JSON array of records (private deployments also accept NDJSON with Content-Type: application/x-ndjson). Every record must carry the context's identity fact; records for the same identity within one batch merge in array order.

[
  { "loan_id": "APP-1", "amount": 12000, "region": "us-east" },
  { "loan_id": "APP-2", "amount": 7300, "region": "eu-west" },
  { "loan_id": "APP-1", "credit_score": 715 }
]

Send chunks sequentially or in parallel from your pipeline. Independent instances can run concurrently. Requests that write overlapping Context state wait for their admitted predecessors; arrival at different clients does not define a global order. Send sequential requests when your application needs an explicit business order.

Instance size and deployment capacity:

Each instance can store 64 MiB of combined state and execution metadata, measured as serialized database JSON. History is stored separately and retains the configured number of entries per tracked fact (default 100, maximum 10,000). There are no separate Context limits on request record counts, accumulated results, relationship expansion, fact counts, tracked-value sizes, or trigger rounds. An update that exceeds the instance limit fails with HTTP 413 and rolls back that database transaction; earlier transactions in the request can already have committed.

Cloud hosting still applies its own HTTP body and execution limits. Private Context batches use the same HTTP admission settings as other HPS routes: the Helm default is 128 MiB per request, controlled by HTTP_BODY_LIMIT_BYTES. Existing HPS overload admission can return HTTP 429 with Retry-After. Rule and flow evaluations count toward usage; merge-only records do not consume rule executions or use remaining executions as a record-count allowance.

Choose batch sizes for the resources available to your deployment. A small incoming patch can read large existing state or related histories, and a large response can consume substantial memory and bandwidth. include reduces the returned fields. Query paging and database chunks control individual operations without imposing an aggregate Context request budget. Runtime deadlines still terminate unfinished work and report known partial progress.

The Response

Each response reports admission results, execution outcomes, and the full resolved state per instance:

{
  "context": "loan-application",
  "accepted": 2,
  "rejected": 0,
  "executed": 1,
  "rejections": [],
  "results": [
    {
      "instance_id": "APP-1",
      "positions": [0, 2],
      "is_new": true,
      "status": "complete",
      "have": ["loan_id", "amount", "credit_score"],
      "need": [],
      "state": { "loan_id": "APP-1", "amount": 12000, "risk_tier": "low" },
      "executed": [
        {
          "type": "rule",
          "slug": "credit-check",
          "status": "success",
          "written_to_context": ["risk_tier"]
        }
      ],
      "triggered": true
    },
    {
      "instance_id": "APP-2",
      "positions": [1],
      "is_new": true,
      "status": "pending",
      "have": ["loan_id", "amount"],
      "need": ["credit_score"],
      "state": { "loan_id": "APP-2", "amount": 7300 },
      "executed": [],
      "triggered": false,
      "reason": "not_ready"
    }
  ]
}

Per-record validation failures appear in rejections with their array position, following the context's on_schema_mismatch setting (a reject context fails individual records carrying unknown facts; ignore drops the unknown facts; store keeps them). Execution outcomes per bound asset are success, evaluation_error, infrastructure_error, or skipped_already_run, and each instance's executions metadata durably records every asset's last input hash, status, run count, and timestamp.

When nothing executed for an instance, triggered is false and reason explains why: not_ready (required facts still missing), no_bound_assets, auto_execute_disabled, execution_unavailable (self-hosted execution backend unreachable), or inputs_unchanged. Assets skipped because their effective version and inputs match their last successful run appear as skipped_already_run; skipped work alone does not set triggered to true.

Record what each response tells you. Since the response is the complete record of what happened to that chunk, your pipeline should capture rejections and execution errors as it goes. There is no server-side job ledger to consult later.

Slimming the Response

By default each result carries the instance's full resolved state, so response size scales with your data. If you only need outcomes, narrow the per-instance fields with ?include=:

POST /api/v1/contexts/batch/loan-application?include=status,executed

instance_id is always present; everything else (positions, is_new, status, have, need, state, expires_at, executions, executed, triggered, reason) is opt-in when include is set. Accumulated executions metadata always requires an explicit include; raw decision results require execution_results. Top-level counts and rejections are unaffected.

Retry Semantics

Retrying a chunk after a timeout or crash makes persisted Context state converge:

  • Merges are idempotent. Re-sending the same facts changes nothing.
  • Executions are deduplicated by effective version and input hash. A pinned target remains pinned. A latest target uses the current published version on the next submission; a new effective version can execute even when its inputs are unchanged. Publication alone does not replay stored instances. An unchanged retry after success skips execution (skipped_already_run); known failed work can be retried.

A failed request can have committed earlier chunks. When known, error responses contain committed_count and committed_instance_ids; do not infer rollback from an HTTP error. A lost response or interrupted write can have an unknown outcome. Ordered work remains fenced when a write cannot be reconciled; repeated blind retries do not clear that fence. Private operators should follow the Context ordering recovery runbook before resuming those identities.

Successful submission responses can also contain execution_degraded, execution errors, or cascade_rejections. Inspect these even when using a narrow include projection. Correct the underlying failure and resubmit the same source facts to revisit dependent work. A failed foreign-key move retains its former targets until that dependent work succeeds. Physical deletion removes that source record: after a reported delete-cascade failure, retry the reported dependent identities directly; an identical delete cannot reconstruct the removed foreign key.

The persistence and decision-result state therefore converge when a failed or interrupted load is re-run. External side effects performed by rules or flows are at least once, not exactly once: if execution completed but its success record was not durably written before interruption, a retry can perform that side effect again. Use idempotency keys in downstream integrations when this matters.

Execution Semantics

Bound rules and flows follow the same triggering model as interactive submissions: an asset executes when its required inputs are present and its effective version or inputs changed. Outputs are written back between rounds. After a flow writes Context state, newly ready assets are reconsidered against fresh stored data. Repeated states and active ancestor flows are bounded to prevent infinite execution; incomplete work is reported explicitly. Rule writebacks cannot change the instance's identity fact.

Context decisions use the existing rule and flow execution machinery. Dependent state operations share their parent ordering ownership. A hot instance therefore has serial state transitions even when other decision work runs in parallel; adding workers does not make one state transition chain arbitrarily fast. Measure independent-instance throughput separately from hot-instance latency.

On private deployments, batches with triggered work distribute complete instance operations across Kafka workers. Independent records within a worker chunk retain bulk database writes; repeated identities retain their input order. Staging-only batches use the direct bulk database path. Batches that change related dependent contexts retain one coordinator to coalesce their cascade. Inputs too large for the configured Kafka item transport execute locally under the same ordering ownership. Context instance state is read from Postgres and is no longer copied into Redis; Redis holds coordination data proportional to outstanding work.

Manual execution mode (auto_execute_decisions: false) is honored: ordinary batches merge data without starting new work. If a manual solve previously returned 202 pending, a later batch that supplies its missing facts can settle that registered rule or flow.

Working with Relationships

Computed facts that aggregate related contexts are supported in bulk mode using either $transactions or $relations.transactions (replace transactions with the relationship name). Relation lookups add per-chunk query cost, so prefer smaller chunks for relation-heavy schemas.

Dependent Contexts Re-Evaluate Automatically

When a batch changes records that other contexts depend on through relationships, those dependents are re-evaluated in the same request: one level deep, coalesced to one evaluation per dependent instance regardless of how many related records changed, and only for instances that already exist. Dependents with auto_execute_decisions: false run only previously registered pending work; other assets remain manual. Dependents whose asset inputs didn't actually change settle as skipped via the same input-hash mechanism. The response summarizes each cascade:

{
  "cascaded": [
    {
      "context": "customers",
      "relation": "transactions",
      "instances": 1840,
      "executed": 1710,
      "skipped": 130,
      "evaluation_errors": 0,
      "infrastructure_errors": 0
    }
  ]
}

Dependency chains deeper than one level still call for an explicit follow-up batch of identity-only payloads ([{ "customer_id": "C-1" }, ...]) per additional level. Input-hash triggering makes that follow-up cheap: only instances whose aggregates actually changed re-execute.

Large dependent sets are paged. Successful pages record progress on the source instance, so retrying unchanged source data after an interruption resumes incomplete work. Changing the source facts starts a fresh pass; completed progress is removed.