Quick answer
PathFinder.search() is a search API, not a movement command. Give it an origin and a goal whose range matches the position your next action actually needs. Then treat result.incomplete as the completion gate. A non-empty result.path is not proof of success because an incomplete search may still return the closest partial path it found.
The safe order is: define the goal contract, run the search, reject incomplete, handle the valid empty-path case, and only then hand a complete non-empty path to movement.
Use the smallest safe search pattern
This example searches from one Creep to one visible Source. The Source tile is not walkable, so the goal stops at range 1:
const creep = Object.values(Game.creeps)[0];
const source = creep?.room.find(FIND_SOURCES)[0];
if (!creep || !source) {
console.log('missing creep or source');
return;
}
const result = PathFinder.search(
creep.pos,
{ pos: source.pos, range: 1 }
);
console.log(JSON.stringify({
incomplete: result.incomplete,
steps: result.path.length,
ops: result.ops,
cost: result.cost
}));
if (result.incomplete) {
console.log('search stopped before reaching the goal range');
return;
}
if (result.path.length === 0) {
console.log('goal range already satisfied; no movement path needed');
return;
}
const moveResult = creep.moveByPath(result.path);
console.log({ moveResult });
There are two separate decisions here. incomplete answers whether the search reached a valid goal state. path.length answers how many positions the returned route contains. Do not reverse those responsibilities.
Make goal range match the task
A PathFinder goal can be a RoomPosition or an object containing pos and range. The documented default range is 0, which means the search tries to reach the target position itself.
That default is wrong for targets that cannot be occupied. The official documentation uses a Source as the example: a Creep cannot stand on the Source tile, so the goal should stop at least one square away.
const sourceGoal = {
pos: source.pos,
range: 1
};
const result = PathFinder.search(creep.pos, sourceGoal);
Use the downstream task to choose the range. If the next operation requires adjacency, range 1 is the natural search contract. If another action can operate farther away, use the range that action actually permits instead of automatically forcing every search to range 1.
harvest(), transfer(), upgradeController(), or any other action.Read path, ops, cost, and incomplete separately
PathFinder.search() returns one object with four fields that answer different questions:
| Field | What it means | Do not treat it as |
|---|---|---|
path | An array of RoomPosition objects for the returned route | A success flag |
ops | Search operations performed before the result was calculated | Path length or travel ticks |
cost | Accumulated search cost from terrain costs and any CostMatrix values | Geometric distance |
incomplete | Whether the pathfinder failed to find a complete path | A hint you can ignore when path is non-empty |
The separation matters when you compare candidates. A route can contain fewer positions but have a higher configured cost. A search can perform many operations and still return a short route. And an incomplete search can return positions that look plausible without ever reaching the required goal range.
Why a non-empty path can still be failure
The easiest PathFinder bug is this:
const result = PathFinder.search(origin, goal);
if (result.path.length > 0) {
assignTask(result.path); // unsafe success test
}
The official API explicitly says that when the pathfinder fails to find a complete path, incomplete becomes true and path can still contain the closest partial path found under the search parameters.
If that partial route is accepted as a valid assignment, the failure moves downstream. A Creep may walk partway toward a target and stop at a room boundary, a blocked area, or a search-limit frontier. The movement layer then looks broken even though the original error was accepting an incomplete search.
Gate the assignment where the evidence exists:
function findCompletePath(origin, goal, opts = {}) {
const result = PathFinder.search(origin, goal, opts);
if (result.incomplete) {
return {
ok: false,
reason: 'incomplete-search',
path: [],
ops: result.ops,
cost: result.cost
};
}
return {
ok: true,
path: result.path,
ops: result.ops,
cost: result.cost
};
}
Keeping the partial path out of the success payload prevents a later caller from accidentally treating it as movement-ready.
Why an empty complete path can be success
The opposite shortcut is also unsafe: path.length === 0 does not automatically mean failure. If the origin already satisfies the goal range, there may be no movement step to return.
const goal = { pos: source.pos, range: 1 };
const result = PathFinder.search(creep.pos, goal);
if (!result.incomplete && result.path.length === 0) {
const alreadyReady = creep.pos.inRangeTo(source.pos, 1);
console.log({ alreadyReady });
}
This gives you a useful invariant: first ask whether the search completed, then ask whether movement is necessary. Do not force both questions into one path-length check.
Know which limits can stop the search
The default search has bounded work. Current official documentation lists maxOps with a default of 2000, maxRooms with a default of 16 and maximum of 64, and maxCost with a default of Infinity. A roomCallback can also return false to exclude a room from the search.
Those controls are useful, but they can also make an otherwise reachable destination incomplete under your chosen search contract. If you deliberately override them, log the exact values rather than treating the following diagnostic example as recommended defaults:
const opts = {
maxOps: 4000,
maxRooms: 8,
maxCost: 250
};
const result = PathFinder.search(creep.pos, goal, opts);
console.log(JSON.stringify({
goalRoom: goal.pos.roomName,
goalRange: goal.range,
maxOps: opts.maxOps,
maxRooms: opts.maxRooms,
maxCost: opts.maxCost,
incomplete: result.incomplete,
steps: result.path.length,
ops: result.ops,
cost: result.cost
}));
Do not respond to every incomplete result by blindly increasing limits. First confirm that the goal range is achievable and that your room or CostMatrix policy did not deliberately block the route.
If you need to build walkability and terrain costs, use the dedicated CostMatrix guide. This article only owns the search-result contract.
Use multiple goals without confusing selection with distance
PathFinder.search() can accept an array of goals. The API returns the cheapest path it finds among them under the configured search costs.
const goals = creep.room.find(FIND_SOURCES).map((source) => ({
pos: source.pos,
range: 1
}));
if (goals.length === 0) {
console.log('no visible sources');
return;
}
const result = PathFinder.search(creep.pos, goals);
if (result.incomplete) {
console.log('no complete goal reached under this search contract');
return;
}
“Cheapest” here follows PathFinder costs; it does not necessarily mean the fewest RoomPositions. If your task also needs load balancing, stable identity, assignment counts, or resource availability, path search is only one input to that policy. The Source selection guide owns that higher-level assignment problem.
Hand a complete path to movement
Path search and movement execution should stay separate. A complete search result says the route reached the requested goal contract. It does not say the Creep has already moved.
const result = PathFinder.search(creep.pos, goal);
if (result.incomplete) {
return;
}
if (result.path.length > 0) {
const moveResult = creep.moveByPath(result.path);
console.log(JSON.stringify({
moveResult,
firstStep: result.path[0],
remainingSteps: result.path.length
}));
}
moveByPath() is a movement call with its own return value. Keep that return value separate from result.incomplete. If movement is accepted but later position does not change, continue with the movement progress diagnostic rather than rewriting the search result.
Debug an incomplete search in the right order
- Confirm origin and goal identity. Log room names and coordinates so you know what was actually searched.
- Confirm goal range. Do not search for an occupied or non-walkable target tile with the default range 0.
- Check
incompletedirectly. Never infer completion from a populated path. - Inspect search limits. Record
maxOps,maxRooms, andmaxCostwhen you override them. - Inspect room exclusion. A
roomCallbackreturningfalsemakes that room unavailable to the search. - Inspect custom costs. A CostMatrix can make tiles effectively unwalkable or make a route exceed your cost budget.
- Only then debug movement. A complete search and an accepted movement command are different stages.
If the failure is specifically ERR_NO_PATH from a movement or routing API, the ERR_NO_PATH guide compares the relevant failure surfaces.
Know what this guide does not own
This article owns one question: how to form a correct PathFinder.search() request and interpret its result before downstream code accepts it.
- For custom tile costs and walkability, use PathFinder CostMatrix.
- For room-level route selection, use Game.map.findRoute().
- For failed path diagnostics across APIs, use ERR_NO_PATH.
- For a Creep that received movement but does not progress, use moveTo() not moving.
- For stable Source assignment policy, use Source selection by path.
This boundary keeps search correctness from turning into a second CostMatrix guide, a second movement diagnostic, or a second target-selection system.
FAQ
Does path.length > 0 mean PathFinder.search() succeeded?
No. An incomplete result may still include a partial path. Check incomplete.
What range should I use for a Source?
At least 1. A Source tile is not walkable, and the official documentation uses range 1 in its example.
Is cost the number of steps?
No. It is the accumulated configured search cost.
Is ops the number of movement ticks?
No. It is the number of search operations performed before the path was calculated.
Can a completed search return an empty path?
Yes when no movement is needed to satisfy the goal contract. Confirm the origin already meets the requested range before deciding what to do next.
Official documentation
PathFinder.use() is deprecated in the current API documentation and is not part of this guide.