RESOURCES · OWNED LAB REACTION

Run One Lab Reaction Without Guessing Which Store Change Was Yours

CURATED PATH

These links are maintained for a concrete prerequisite, failure mode, debugging step, or next lesson instead of generic topic similarity.

Market / Industry

Recommended next steps

Connect compounds to actual Creep boostsDownstream useTrace reagents back to mineral harvestingUpstream resourceContinue into broader industrial productionAdvanced production
Verification statusChinese source article: Reviewed in full · Official documentation: Checked August 14, 2026 — StructureLab.runReaction(), REACTIONS, LAB_REACTION_AMOUNT, PWR_OPERATE_LAB, POWER_INFO, range, cooldown and return codes · Official implementation source: Checked current screeps/engine runReaction validation and processor — an active PWR_OPERATE_LAB effect increases reactionAmount; the calling output Lab must be owned/active, while input Lab ownership/activity is not an API precondition

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official documentation
Checked August 14, 2026 — StructureLab.runReaction(), REACTIONS, LAB_REACTION_AMOUNT, PWR_OPERATE_LAB, POWER_INFO, range, cooldown and return codes
Official implementation source
Checked current screeps/engine runReaction validation and processor — an active PWR_OPERATE_LAB effect increases reactionAmount; the calling output Lab must be owned/active, while input Lab ownership/activity is not an API precondition
Static code review
Passed — dynamic reaction amount, reagent and capacity preflight, exact input/output deltas, shared-input risk, exclusive window, and output-vs-input API boundaries reviewed
Screeps Console test
Pending
Live multi-tick verification
Pending
Live boundary test
Pending — Live shared-input, exclusive-window and exact three-Store delta test; no live PWR_OPERATE_LAB trace was collected
Evidence level
Official documentation, official implementation source, repository review, JavaScript review and static lifecycle analysis only
Last editorial review
August 14, 2026

Use this guide when

Use this workflow when one scheduler must submit one runReaction() call and later prove which Store changes came from that reaction. The important 2026 boundary is that the produced amount is not always the base LAB_REACTION_AMOUNT: an active PWR_OPERATE_LAB effect on the output Lab increases the amount processed by the engine.

If preflight, snapshots, and verification all hard-code the base amount, the code becomes internally inconsistent exactly when an operated Lab is most valuable.

Separate output Lab requirements from input Lab requirements

function getVisibleLab(id) {
  const lab = typeof id === 'string'
    ? Game.getObjectById(id)
    : null;

  return lab && lab.structureType === STRUCTURE_LAB
    ? lab
    : null;
}

function getOwnedActiveOutputLab(id) {
  const lab = getVisibleLab(id);

  return lab
    && lab.my === true
    && lab.isActive() === true
      ? lab
      : null;
}

The Lab that calls runReaction() is the output Lab and must satisfy the ownership/activity checks enforced by the API. The current engine does not impose the same ownership or isActive() precondition on the two input Lab objects. Requiring owned active input Labs can still be a sensible project policy for a closed production room, but label it as your policy rather than the API contract.

All three Labs must still be valid, distinct Lab objects, and each input must be within range 2 of the output Lab.

Calculate the reaction amount from the output Lab

function getReactionAmount(outputLab) {
  let reactionAmount = LAB_REACTION_AMOUNT;
  const effect = outputLab.effects?.find(item =>
    item.effect === PWR_OPERATE_LAB
    && item.ticksRemaining > 0
  );

  if (effect) {
    reactionAmount +=
      POWER_INFO[PWR_OPERATE_LAB].effect[effect.level - 1];
  }

  return reactionAmount;
}

This mirrors the current official engine boundary: start with LAB_REACTION_AMOUNT, then add the active PWR_OPERATE_LAB effect for the output Lab. Snapshot that value once for the request. Do not calculate one amount for preflight and a different amount for settlement.

Build a plan from current reagents, amount, range, and capacity

function getReactionProduct(inputA, inputB) {
  const reagentA = inputA.mineralType;
  const reagentB = inputB.mineralType;

  if (!reagentA || !reagentB) return null;

  return {
    reagentA,
    reagentB,
    product: REACTIONS[reagentA]?.[reagentB] ?? null
  };
}

function buildReactionPlan(request) {
  const outputLab = getOwnedActiveOutputLab(request.outputLabId);
  const inputA = getVisibleLab(request.inputLabAId);
  const inputB = getVisibleLab(request.inputLabBId);

  if (!outputLab || !inputA || !inputB) {
    return { ready: false, reason: 'lab-unavailable' };
  }

  if (
    outputLab.id === inputA.id
    || outputLab.id === inputB.id
    || inputA.id === inputB.id
  ) {
    return { ready: false, reason: 'lab-ids-not-distinct' };
  }

  if (
    !outputLab.pos.inRangeTo(inputA, 2)
    || !outputLab.pos.inRangeTo(inputB, 2)
  ) {
    return { ready: false, reason: 'input-out-of-range' };
  }

  if (outputLab.cooldown > 0) {
    return { ready: false, reason: 'output-cooling-down' };
  }

  const recipe = getReactionProduct(inputA, inputB);
  if (!recipe?.product) {
    return { ready: false, reason: 'recipe-invalid' };
  }

  const reactionAmount = getReactionAmount(outputLab);

  if (
    inputA.store.getUsedCapacity(recipe.reagentA) < reactionAmount
    || inputB.store.getUsedCapacity(recipe.reagentB) < reactionAmount
  ) {
    return {
      ready: false,
      reason: 'reagent-insufficient',
      product: recipe.product,
      reactionAmount
    };
  }

  if (
    outputLab.mineralType
    && outputLab.mineralType !== recipe.product
  ) {
    return { ready: false, reason: 'output-mineral-conflict' };
  }

  if (
    outputLab.store.getFreeCapacity(recipe.product) < reactionAmount
  ) {
    return { ready: false, reason: 'output-capacity-insufficient' };
  }

  return {
    ready: true,
    reason: 'ready',
    outputLab,
    inputA,
    inputB,
    ...recipe,
    reactionAmount
  };
}

Using LAB_REACTION_AMOUNT directly in the reagent or output-capacity checks is only correct when no active operation effect increases the batch. The plan carries reactionAmount forward so every later check uses the same request identity.

Reserve one exclusive three-Store window

Between snapshot and verification, prevent haulers, another reaction scheduler, and any other code from changing the product or reagent amounts in these three Labs. Shared input Labs are legal, but simultaneous consumers make exact attribution impossible unless the scheduler serializes them.

Record exclusiveWindow: true only when the production coordinator actually enforces that promise.

Snapshot the exact amount and submit once

function submitReactionRequest(request) {
  const plan = buildReactionPlan(request);

  request.lastCheckedAt = Game.time;
  request.lastStatus = plan.reason;

  if (!plan.ready) {
    return { status: 'not-submitted', reason: plan.reason };
  }

  request.enabled = false;
  request.snapshot = {
    exclusiveWindow: request.exclusiveWindow === true,
    outputLabId: plan.outputLab.id,
    inputLabAId: plan.inputA.id,
    inputLabBId: plan.inputB.id,
    reagentA: plan.reagentA,
    reagentB: plan.reagentB,
    product: plan.product,
    reactionAmount: plan.reactionAmount,
    outputBefore:
      plan.outputLab.store.getUsedCapacity(plan.product),
    inputABefore:
      plan.inputA.store.getUsedCapacity(plan.reagentA),
    inputBBefore:
      plan.inputB.store.getUsedCapacity(plan.reagentB)
  };

  const result = plan.outputLab.runReaction(
    plan.inputA,
    plan.inputB
  );

  request.result = result;
  request.resultAt = Game.time;
  request.status = result === OK
    ? 'accepted-pending-reaction'
    : 'failed-review-required';

  return { status: request.status, result };
}

OK means the reaction intent was accepted. It is not a license to infer later Store contents without checking them.

Verify one output increase and both reagent decreases

function verifyReaction(request) {
  const snapshot = request?.snapshot;

  if (!snapshot || request.result !== OK) {
    return { status: 'submission-not-accepted' };
  }

  if (snapshot.exclusiveWindow !== true) {
    return { status: 'not-exclusive' };
  }

  const outputLab = getVisibleLab(snapshot.outputLabId);
  const inputA = getVisibleLab(snapshot.inputLabAId);
  const inputB = getVisibleLab(snapshot.inputLabBId);

  if (!outputLab || !inputA || !inputB) {
    return { status: 'lab-unavailable' };
  }

  const outputDelta =
    outputLab.store.getUsedCapacity(snapshot.product)
      - snapshot.outputBefore;
  const inputADelta =
    inputA.store.getUsedCapacity(snapshot.reagentA)
      - snapshot.inputABefore;
  const inputBDelta =
    inputB.store.getUsedCapacity(snapshot.reagentB)
      - snapshot.inputBBefore;

  const exact =
    outputDelta === snapshot.reactionAmount
    && inputADelta === -snapshot.reactionAmount
    && inputBDelta === -snapshot.reactionAmount;

  if (exact) {
    return {
      status: 'verified-exact-reaction',
      outputDelta,
      inputADelta,
      inputBDelta
    };
  }

  const unchanged =
    outputDelta === 0
    && inputADelta === 0
    && inputBDelta === 0;

  return {
    status: unchanged
      ? 'pending-no-store-delta'
      : 'state-changed-ambiguous',
    outputDelta,
    inputADelta,
    inputBDelta
  };
}

The exact signature is +reactionAmount product and -reactionAmount from each input. Cooldown is supporting evidence, not a unique reaction identifier.

Keep acceptance and settlement states separate

StatusMeaning
accepted-pending-reactionrunReaction() returned OK; exact Store evidence is still pending.
pending-no-store-deltaThe later snapshot is unchanged; keep investigating timing and visibility instead of inventing success.
state-changed-ambiguousAt least one Store changed but the three deltas do not match one isolated reaction.
verified-exact-reactionProduct and both reagents match the one snapshotted reaction amount.

Preserve the documented return codes

Documented runReaction() results include ERR_NOT_OWNER, ERR_NOT_ENOUGH_RESOURCES, ERR_INVALID_TARGET, ERR_FULL, ERR_NOT_IN_RANGE, ERR_INVALID_ARGS, ERR_TIRED, and ERR_RCL_NOT_ENOUGH. Log the actual return code instead of collapsing every failure into “reaction failed.”

Keep hauling, boosting, and reaction scheduling separate

This page owns one reaction request and its evidence. Lab assignment, reagent hauling, reaction chains, reverse reactions, boost queues, and multi-output scheduling need separate state machines. Continue with the boost verification guide when the product is intended for a Creep.

Official documentation and source

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide keeps the technical scope and verification boundaries of its Chinese source. The verification record shows which checks were completed and which runtime observations are still pending.