RESOURCES · LAB REACTION WORKFLOW

How to Run Lab Reactions Safely in Screeps

Validate three owned active Labs, resolve the REACTIONS recipe, check reagent stores, output mineral compatibility, free capacity, range 2, cooldown, and a one-time production request before calling runReaction() and verifying the next tick.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — runReaction(), REACTIONS, LAB_REACTION_AMOUNT, REACTION_TIME, range and return codes · Recipe boundary: The product is derived from current input mineral types; no recipe name is trusted from Memory alone

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — runReaction(), REACTIONS, LAB_REACTION_AMOUNT, REACTION_TIME, range and return codes
Recipe boundary
The product is derived from current input mineral types; no recipe name is trusted from Memory alone
Execution boundary
OK means the reaction was scheduled; output Store and cooldown require next-tick verification
JavaScript syntax
Passed
Offline reaction review
Passed — missing Lab, ownership, activity, recipe, reagent amount, output compatibility, capacity, range and cooldown states
Screeps Console test
Pending
Live reaction, Store delta and cooldown test
Pending
Last verified
July 26, 2026

Quick answer

Recover three Lab objects from IDs, require ownership and activity, read the two input mineral types, resolve the product through REACTIONS, check at least LAB_REACTION_AMOUNT of both reagents, compatible output mineral, free capacity, range 2, and zero cooldown. Disable the one-time request before outputLab.runReaction(inputA, inputB), then verify the output Store and cooldown on the next tick.

Use two input Labs and one output Lab

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

  if (
    !lab
    || lab.structureType !== STRUCTURE_LAB
    || lab.my !== true
    || lab.isActive() !== true
  ) {
    return null;
  }

  return lab;
}

The three IDs must resolve to three distinct Labs in the same room. The output Lab is the object that calls runReaction().

Resolve the recipe from REACTIONS

function getReactionProduct(inputA, inputB) {
  if (
    typeof inputA !== 'string'
    || typeof inputB !== 'string'
  ) {
    return null;
  }

  return REACTIONS[inputA]?.[inputB] ?? null;
}

Do not trust a product string stored in Memory without comparing it to the current reagent mineral types.

Check reagent amount and output capacity

function getLabMineral(lab) {
  const mineralType = lab.mineralType;

  return mineralType
    ? {
        type: mineralType,
        amount: lab.store.getUsedCapacity(mineralType)
      }
    : null;
}

function canReceiveReaction(outputLab, product) {
  if (
    outputLab.mineralType
    && outputLab.mineralType !== product
  ) {
    return false;
  }

  return outputLab.store.getFreeCapacity(product)
    >= LAB_REACTION_AMOUNT;
}

Each input needs at least LAB_REACTION_AMOUNT of its reagent. The output Lab needs enough capacity for that same reaction amount.

Check range and cooldown

function labsInReactionRange(outputLab, inputA, inputB) {
  return outputLab.pos.inRangeTo(inputA, 2)
    && outputLab.pos.inRangeTo(inputB, 2);
}

The output Lab must have cooldown === 0. The cooldown after a successful reaction is determined by REACTION_TIME[product].

Build a testable reaction plan

function evaluateReaction(input) {
  const { outputLab, inputA, inputB } = input;

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

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

  if (
    outputLab.room.name !== inputA.room.name
    || outputLab.room.name !== inputB.room.name
  ) {
    return { ready: false, reason: 'different-rooms' };
  }

  const reagentA = getLabMineral(inputA);
  const reagentB = getLabMineral(inputB);

  if (!reagentA || !reagentB) {
    return { ready: false, reason: 'reagent-missing' };
  }

  const product = getReactionProduct(
    reagentA.type,
    reagentB.type
  );

  if (!product) {
    return { ready: false, reason: 'recipe-invalid' };
  }

  if (
    reagentA.amount < LAB_REACTION_AMOUNT
    || reagentB.amount < LAB_REACTION_AMOUNT
  ) {
    return { ready: false, reason: 'reagent-insufficient', product };
  }

  if (!canReceiveReaction(outputLab, product)) {
    return { ready: false, reason: 'output-unavailable', product };
  }

  if (!labsInReactionRange(outputLab, inputA, inputB)) {
    return { ready: false, reason: 'input-out-of-range', product };
  }

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

  return { ready: true, reason: 'ready', product };
}

Complete one-time reaction example

State impact: this code may schedule one real Lab reaction and writes a request snapshot. It never retries automatically.

module.exports.loop = function () {
  const request = Memory.labReactionRequest;

  if (!request || request.enabled !== true) {
    return;
  }

  const outputLab = getOwnedActiveLab(request.outputLabId);
  const inputA = getOwnedActiveLab(request.inputLabAId);
  const inputB = getOwnedActiveLab(request.inputLabBId);
  const plan = evaluateReaction({ outputLab, inputA, inputB });

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

  if (!plan.ready) {
    request.preview = { product: plan.product ?? null };
    return;
  }

  request.enabled = false;
  request.submittedAt = Game.time;
  request.snapshot = {
    outputLabId: outputLab.id,
    inputLabAId: inputA.id,
    inputLabBId: inputB.id,
    product: plan.product,
    outputBefore:
      outputLab.store.getUsedCapacity(plan.product),
    inputABefore:
      inputA.store.getUsedCapacity(inputA.mineralType),
    inputBBefore:
      inputB.store.getUsedCapacity(inputB.mineralType),
    cooldownBefore: outputLab.cooldown
  };

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

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

Verify output on the next tick

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

  if (!snapshot || !outputLab) {
    return { verified: false, reason: 'snapshot-or-lab-missing' };
  }

  const outputNow = outputLab.store.getUsedCapacity(
    snapshot.product
  );

  return {
    verified:
      request.result === OK
      && outputNow >= snapshot.outputBefore
        + LAB_REACTION_AMOUNT,
    outputDelta: outputNow - snapshot.outputBefore,
    cooldownNow: outputLab.cooldown
  };
}

Traffic, hauling, another reaction module, or resource withdrawal can change the Store before verification. A production system should coordinate Lab ownership across modules.

Handle return codes

CodeMeaning
OKReaction scheduled
ERR_NOT_ENOUGH_RESOURCESInput reagent unavailable
ERR_INVALID_TARGETInput target is not a valid Lab
ERR_FULLOutput cannot receive product
ERR_NOT_IN_RANGEAn input Lab is outside range 2
ERR_INVALID_ARGSReagents do not form a valid product
ERR_TIREDOutput Lab is cooling down

Debugging checklist

  • Recover three distinct owned active Labs.
  • Require one room.
  • Read current input mineral types.
  • Resolve the product from REACTIONS.
  • Check both reagent amounts.
  • Check output mineral compatibility and capacity.
  • Check range 2 and cooldown.
  • Disable before the call.
  • Save Store snapshots and the return code.
  • Verify output and cooldown on the next tick.

Scope and next steps

This guide does not implement reaction chains, Lab assignment, hauling, reverse reactions, demand forecasting, or multi-output scheduling. Continue with boosting eligible Creep parts.

Frequently asked questions

Can input Lab order be reversed?

Use the current REACTIONS lookup instead of assuming a recipe order.

Can several output Labs share inputs?

Yes when each output is in range and resources are sufficient.

Should a failed reaction retry every tick?

No. Require a new reviewed request.

Is cooldown a proof of success?

It is supporting evidence; compare Store deltas and coordinate other Lab users.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide is rewritten for a focused search intent while preserving the technical scope and verification boundaries of its Chinese source. Live-room evidence is claimed only when the verification record says it exists.