CONSTRUCTION · BUILDER ACTION ARBITRATION

Run a Screeps Builder Without Hiding Build, Repair, or Controller Errors

CURATED PATH

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

Construction

Recommended next steps

Start from the first ExtensionPrerequisite tutorialVerify the Builder's actual progressResult verificationCheck build and repair eventsRuntime evidenceUse Room Diagnostics when a Builder is not making progressFailure diagnosisHandle Container decay repair deadlinesAdvanced failure
Verification statusChinese source article: Reviewed in full · Official documentation: Checked August 14, 2026 — Creep.build(), Creep.repair(), Creep.upgradeController(), Store, FIND_MY_CONSTRUCTION_SITES and Controller ownership · Official engine source: Checked current screeps/engine: repair() does not require target-structure ownership, build() can reject occupied obstacle sites, and upgradeController() requires an owned valid Controller and rejects upgradeBlocked

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official documentation
Checked August 14, 2026 — Creep.build(), Creep.repair(), Creep.upgradeController(), Store, FIND_MY_CONSTRUCTION_SITES and Controller ownership
Official engine source
Checked current screeps/engine: repair() does not require target-structure ownership, build() can reject occupied obstacle sites, and upgradeController() requires an owned valid Controller and rejects upgradeBlocked
Static code review
Passed — task selection, action submission, movement, ownership, upgradeBlocked, target policy, and later evidence are separated; lower priorities do not hide a selected task's hard error
Screeps Console test
Pending
Live multi-tick verification
Pending
Live boundary test
Pending — no live blocked-site, neutral-Controller, upgradeBlocked, repair-delta, or multi-tick task-priority trace was collected
Evidence level
Official documentation, official engine source, repository review and offline reasoning only
Last editorial review
August 14, 2026

Quick answer

A beginner Builder can use the priority build → repair → upgrade, but target existence is not enough. Select one task, submit that method, preserve its exact return code, and only use Controller upgrading when the room has a Controller that is actually yours and is not currently upgradeBlocked.

Do not let a failed high-priority build silently fall through to repair or upgrading in the same control branch. That makes a visible failure disappear from your diagnostics.

One selected task per tick

PrioritySelection ruleSubmitted action
1An owned Construction Site is selected.build(site)
2No site; a structure passes this project's repair policy.repair(structure)
3No higher target; current room Controller is owned and upgradeable.upgradeController(controller)

“One selected task” is an application policy. Screeps still has its own intent rules. The point of returning after a selected branch is to keep this lesson deterministic and preserve the first failure you actually need to debug.

Repair target policy is not the same as a repair() ownership rule

This article intentionally repairs:

  • structures where my === true;
  • Roads;
  • Containers;

and excludes Walls/Ramparts so a beginner maintenance worker does not spend its entire budget on defensive hit pools.

That filter is project policy. The current official Creep.repair() submission and processor do not require the target structure itself to be yours. Roads and Containers therefore do not need to be mislabeled as owned just to explain why this policy includes them.

Guard the Controller fallback before calling upgradeController()

The previous lesson only checked whether creep.room.controller existed. A visible neutral or hostile Controller is still a Controller object, but the current upgradeController() boundary requires the target to be yours. The method also rejects a Controller whose upgradeBlocked value is active.

function getUpgradeTarget(creep) {
  const controller = creep.room.controller;

  if (!controller) {
    return {
      status: 'controller-missing',
      controller: null
    };
  }

  if (controller.my !== true) {
    return {
      status: 'controller-not-owned',
      controller: null
    };
  }

  if (controller.upgradeBlocked > 0) {
    return {
      status: 'controller-upgrade-blocked',
      controller: null,
      upgradeBlocked: controller.upgradeBlocked
    };
  }

  return {
    status: 'controller-ready',
    controller
  };
}

This preflight improves diagnostics. The real method return code must still be checked because state can change and the API remains the authority on the submitted action.

A safer Builder loop

const CREEP_NAME = 'Builder1';

function moveToRange(creep, target, range) {
  return creep.moveTo(target, {
    range,
    reusePath: 5
  });
}

function selectRepairTarget(creep) {
  return creep.room.find(FIND_STRUCTURES, {
    filter: structure => {
      const allowed =
        structure.my === true
        || structure.structureType === STRUCTURE_ROAD
        || structure.structureType === STRUCTURE_CONTAINER;

      const defense =
        structure.structureType === STRUCTURE_WALL
        || structure.structureType === STRUCTURE_RAMPART;

      return allowed
        && !defense
        && structure.hits < structure.hitsMax;
    }
  })[0] || null;
}

function runWork(creep) {
  const site =
    creep.room.find(FIND_MY_CONSTRUCTION_SITES)[0];

  if (site) {
    const actionResult = creep.build(site);
    let moveResult = null;

    if (actionResult === ERR_NOT_IN_RANGE) {
      moveResult = moveToRange(creep, site, 3);
    }

    return {
      task: 'build',
      targetId: site.id,
      actionResult,
      moveResult
    };
  }

  const damaged = selectRepairTarget(creep);

  if (damaged) {
    const actionResult = creep.repair(damaged);
    let moveResult = null;

    if (actionResult === ERR_NOT_IN_RANGE) {
      moveResult = moveToRange(creep, damaged, 3);
    }

    return {
      task: 'repair',
      targetId: damaged.id,
      actionResult,
      moveResult
    };
  }

  const upgradeTarget = getUpgradeTarget(creep);

  if (!upgradeTarget.controller) {
    return {
      task: 'upgrade',
      actionResult: null,
      moveResult: null,
      status: upgradeTarget.status,
      upgradeBlocked:
        upgradeTarget.upgradeBlocked ?? null
    };
  }

  const controller = upgradeTarget.controller;
  const actionResult =
    creep.upgradeController(controller);
  let moveResult = null;

  if (actionResult === ERR_NOT_IN_RANGE) {
    moveResult = moveToRange(creep, controller, 3);
  }

  return {
    task: 'upgrade',
    targetId: controller.id,
    actionResult,
    moveResult,
    status: 'upgrade-submitted'
  };
}

function runBuilder(creep) {
  const usedEnergy =
    creep.store.getUsedCapacity(RESOURCE_ENERGY) || 0;
  const freeEnergy =
    creep.store.getFreeCapacity(RESOURCE_ENERGY) || 0;

  if (usedEnergy === 0) {
    creep.memory.working = false;
  } else if (freeEnergy === 0) {
    creep.memory.working = true;
  }

  if (creep.memory.working) {
    return runWork(creep);
  }

  const source =
    creep.pos.findClosestByPath(FIND_SOURCES_ACTIVE);

  if (!source) {
    return { task: 'harvest', status: 'no-active-source' };
  }

  const actionResult = creep.harvest(source);
  let moveResult = null;

  if (actionResult === ERR_NOT_IN_RANGE) {
    moveResult = moveToRange(creep, source, 1);
  }

  return {
    task: 'harvest',
    targetId: source.id,
    actionResult,
    moveResult
  };
}

module.exports.loop = function () {
  const creep = Game.creeps[CREEP_NAME];

  if (!creep || creep.spawning) {
    return;
  }

  const result = runBuilder(creep);

  if (
    result.actionResult !== undefined
    && result.actionResult !== null
    && result.actionResult !== OK
    && result.actionResult !== ERR_NOT_IN_RANGE
  ) {
    console.log(JSON.stringify({
      tick: Game.time,
      creep: creep.name,
      ...result
    }));
  }
};

The code does not submit repair or upgrade after a selected build fails. It reports that build result and re-evaluates current state on the next tick.

Keep each action result method-specific

A generic “workResult” message loses context because the same integer can mean different operational problems depending on which method produced it. Record the method or task with the result.

MethodImportant hard states in this workflow
build()No Energy, no active WORK, range failure, invalid/stale/blocked site.
repair()No Energy, no active WORK, invalid structure, range failure.
upgradeController()Not owned, upgrade blocked/invalid target, no Energy, no active WORK, range failure.

OK means that method's intent was accepted. It is not the later proof that construction progress, structure hits, or Controller progress changed by the amount you expected.

Build ERR_INVALID_TARGET needs its own diagnostic branch

For obstacle-structure Construction Sites, including an Extension site, the current engine can return ERR_INVALID_TARGET when blocking objects or Creeps occupy the target tile. A disappeared or replaced site can produce the same broad return family.

When this happens, re-resolve the site and inspect occupants before falling back to another task. The first Extension guide covers that boundary in detail.

Repair can target selected non-owned infrastructure

The current repair() API checks whether the acting Creep is yours, whether it has active WORK, Energy, a valid structure target, and range. It does not require the target structure itself to have my === true.

That is why a project can deliberately include Roads and Containers in a repair policy. It should still avoid “repair everything damaged” as a default: target selection, Energy reserve policy, and defensive hit targets are application decisions.

Controller fallback must be owned and currently upgradeable

The current engine checks upgradeBlocked and returns ERR_INVALID_TARGET while the block is active. It also returns ERR_NOT_OWNER for a Controller that is not yours.

Keep those states different in your own telemetry:

  • controller-missing — no Controller object in the current room;
  • controller-not-owned — a Controller exists but is not a valid upgrade target for this Builder;
  • controller-upgrade-blocked — yours, but temporarily blocked;
  • upgrade-submitted — the API was actually called; inspect its return code.

Verify the selected task after intent processing

For a controlled test, snapshot only the evidence needed by the selected task:

  • build: site ID, progress, progressTotal and target-tile occupants;
  • repair: structure ID, hits and hitsMax;
  • upgrade: Controller ID, ownership, upgradeBlocked and progress fields;
  • all tasks: Creep Energy, active WORK, range and exact action/movement return codes.

Then inspect the later game state. Do not infer a precise state delta solely from OK, especially when another Creep could act on the same target.

Why a hard high-priority error should not trigger lower-priority work immediately

If a Construction Site exists but build() returns a hard error, immediately repairing something else can make the room look “busy” while the construction bug remains hidden. This lesson returns the selected task result and lets the next tick re-evaluate.

Production AI can choose a different failover policy, but it should make that policy explicit and preserve the original failure evidence.

Scope and known limits

  • No nearest-target or path-cost sorting.
  • No multi-Builder reservation or assignment.
  • No Wall/Rampart hit policy.
  • No per-structure repair reserve budget.
  • No claim/reserve Controller logic; the fallback is owned-room upgrading only.
  • No live multi-tick trace was collected in this editorial pass.

Official documentation and engine 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.