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
| Priority | Selection rule | Submitted action |
|---|---|---|
| 1 | An owned Construction Site is selected. | build(site) |
| 2 | No site; a structure passes this project's repair policy. | repair(structure) |
| 3 | No 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.
| Method | Important 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,progressTotaland target-tile occupants; - repair: structure ID,
hitsandhitsMax; - upgrade: Controller ID, ownership,
upgradeBlockedand 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.