MOVEMENT · CREEP PULL

How to Make Creep.pull() Actually Move the Target

CURATED PATH

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

Movement

Recommended next steps

Understand fatigue and MOVE load firstPrerequisite conceptDebug a pull pair that makes no progressFailure modeConfirm adjacency and distance rulesRange check
Verification statusArticle origin: Original English guide — no translated source article · Official API docs: Checked — Creep.pull() and Creep.move() coordination, range, fatigue, and documented return codes · Public engine source: Checked — pull intent registration, pulled-Creep movement eligibility, and API-layer return behavior

VERIFICATION

Evidence and test status

Article origin
Original English guide — no translated source article
Official API docs
Checked — Creep.pull() and Creep.move() coordination, range, fatigue, and documented return codes
Public engine source
Checked — pull intent registration, pulled-Creep movement eligibility, and API-layer return behavior
Code review
Passed — 5 JavaScript blocks syntax checked and coordinator guards reviewed
Deterministic coordinator cases
Passed — 21 assertions across validation, staged failures, and success ordering
Screeps Console test
Pending — no Console execution is claimed
Live shard movement test
Pending — no live pair movement is claimed
Last verified
August 29, 2026

Quick answer

Creep.pull() is not an automatic drag command. A working pull step is a coordinated movement contract between two adjacent Creeps. In the same tick, the puller must schedule its own movement, call pull(target), and the target must call move(puller).

Preserve the return value from every call. OK means the current-tick intent was accepted by that API boundary; it does not prove that both positions changed. Store the pair identity and starting positions if you need to verify the processed movement on a later tick.

Use the three-intent pattern

The official API example uses this ordering: move the puller, establish the pull, then make the target move toward the puller. This minimal fragment assumes you have already selected two adjacent Creeps and a valid direction:

const pullerMoveResult = puller.move(direction);
const pullResult = puller.pull(target);
const targetMoveResult = target.move(puller);

console.log(JSON.stringify({
  pullResult,
  pullerMoveResult,
  targetMoveResult
}));

All three results matter. If the puller's move fails, there is no useful destination step. If pull() fails, there is no valid pull relationship. If target.move(puller) fails, the target did not submit the special movement intent that completes the documented pair pattern.

Why pull() alone does not move the target

pull(target) establishes the relationship used by movement processing. It does not choose where the puller should go, and it does not submit the target's movement intent for you. That is why code like puller.pull(target) by itself can return OK without becoming a complete movement instruction.

The target's call is also unusual. Creep.move() normally receives a direction from 1 through 8, but the API also accepts a nearby Creep object for pulling. In that special form, the official documentation says the target's ERR_TIRED and ERR_NO_BODYPART checks are bypassed.

const targetMoveResult = target.move(puller);

if (targetMoveResult !== OK) {
  console.log(JSON.stringify({ targetMoveResult }));
}

That exception belongs to the pulled target's special move(puller) call. It does not make the puller's own directional movement immune to fatigue, missing MOVE parts, obstacles, or normal movement conflicts.

Check the pair before scheduling movement

A robust coordinator should reject impossible or ambiguous pair state before emitting any movement intent:

  • Both objects exist and are Creeps you intend to coordinate. Do not keep stale names or IDs without resolving them again on the current tick.
  • The pair is adjacent. pull() returns ERR_NOT_IN_RANGE when the target is not next to the puller.
  • Neither Creep is still spawning. A spawning Creep cannot participate in this pair step.
  • The target is not the puller itself. Self-pulling is not a valid target.
  • The puller has an active MOVE part and no current fatigue. The puller must still submit a normal directional move.
  • Your coordinator controls both Creeps. The helper below requires target.my because it must call target.move(puller). This is a coordinator policy, not a claim that pull() itself documents an ownership error for the target.

If your problem is the puller's ordinary movement speed rather than pair coordination, use the movement-speed and fatigue guide instead of hiding that calculation inside the pull helper.

Read pull() return codes without inventing one

The current official Creep.pull() return table documents these outcomes:

ReturnMeaning for this callNext check
OKThe pull intent was accepted at the API boundary.Check the two movement calls, then verify positions later if the result matters.
ERR_NOT_OWNERYou do not own the puller.Resolve the Creep identity and ownership before scheduling it.
ERR_BUSYThe puller is still spawning.Wait until the Creep exists as an active unit.
ERR_INVALID_TARGETThe target is not a valid other Creep for this pull.Check object type, spawning state, and self-targeting.
ERR_NOT_IN_RANGEThe target is not adjacent.Move the pair into adjacency before trying the pull step.

The same documentation says pulling requires a MOVE body part, but its pull() return table does not list ERR_NO_BODYPART. The current public Screeps engine API layer likewise registers pull() after ownership, spawning, target, and adjacency checks without emitting that code.

Practical boundary: validate an active MOVE part on the puller before scheduling the pair. Do not manufacture an undocumented pull() return branch to represent that precondition.

Understand where pulling fatigue goes

Pulling does not make movement weight disappear. The official API says the fatigue generated for the target's movement is added to the puller instead of the target. That makes the puller's body, the target's body and carried resources, terrain, roads, and boosts relevant to sustained pull speed.

This guide deliberately does not promise a fixed travel rate for a pull pair. A useful speed statement needs the actual bodies, active parts, carried resources, terrain sequence, roads, boosts, and the pair's observed movement. If you only need the general fatigue math, keep that concern in the dedicated movement-speed guide.

Use a coordinator that preserves each failure stage

The helper below validates the state this pair coordinator depends on, then submits each call in the documented order. It returns the exact stage that failed rather than collapsing everything into a boolean:

function schedulePullStep(puller, target, direction) {
  if (!puller || !target) {
    return { ok: false, stage: 'validate', reason: 'missing-creep' };
  }

  if (!puller.my || !target.my) {
    return { ok: false, stage: 'validate', reason: 'pair-not-owned' };
  }

  if (puller.spawning || target.spawning) {
    return { ok: false, stage: 'validate', reason: 'spawning' };
  }

  if (puller.id === target.id) {
    return { ok: false, stage: 'validate', reason: 'same-creep' };
  }

  if (!puller.pos.isNearTo(target.pos)) {
    return { ok: false, stage: 'validate', reason: 'not-adjacent' };
  }

  if (puller.getActiveBodyparts(MOVE) === 0) {
    return { ok: false, stage: 'validate', reason: 'puller-no-move' };
  }

  if (puller.fatigue > 0) {
    return { ok: false, stage: 'validate', reason: 'puller-tired' };
  }

  if (!Number.isInteger(direction) || direction < 1 || direction > 8) {
    return { ok: false, stage: 'validate', reason: 'invalid-direction' };
  }

  const pullerMoveResult = puller.move(direction);
  if (pullerMoveResult !== OK) {
    return { ok: false, stage: 'puller-move', code: pullerMoveResult };
  }

  const pullResult = puller.pull(target);
  if (pullResult !== OK) {
    return { ok: false, stage: 'pull', code: pullResult };
  }

  const targetMoveResult = target.move(puller);
  if (targetMoveResult !== OK) {
    return { ok: false, stage: 'target-move', code: targetMoveResult };
  }

  return {
    ok: true,
    stage: 'scheduled',
    codes: {
      pull: pullResult,
      pullerMove: pullerMoveResult,
      targetMove: targetMoveResult
    }
  };
}

Requiring target.my is intentional here: this function owns both movement calls. If your architecture does not control the target's code, it cannot guarantee the three-intent contract and should not report a fully scheduled pair.

Verify actual movement on a later tick

Current-tick return values are not later-tick position evidence. If the operation matters enough to verify, store the exact Creep IDs, starting positions, tick, and current-tick result. On a later tick, resolve those IDs again before comparing positions:

function rememberPullAttempt(puller, target, result) {
  Memory.pullCheck = {
    tick: Game.time,
    pullerId: puller.id,
    targetId: target.id,
    pullerStart: {
      x: puller.pos.x,
      y: puller.pos.y,
      roomName: puller.pos.roomName
    },
    targetStart: {
      x: target.pos.x,
      y: target.pos.y,
      roomName: target.pos.roomName
    },
    result
  };
}

function positionChanged(start, creep) {
  return Boolean(creep) && (
    creep.pos.x !== start.x ||
    creep.pos.y !== start.y ||
    creep.pos.roomName !== start.roomName
  );
}

function inspectPreviousPull() {
  const check = Memory.pullCheck;
  if (!check || check.tick >= Game.time) return null;

  const puller = Game.getObjectById(check.pullerId);
  const target = Game.getObjectById(check.targetId);

  return {
    scheduledAt: check.tick,
    pullerExists: Boolean(puller),
    targetExists: Boolean(target),
    pullerMoved: positionChanged(check.pullerStart, puller),
    targetMoved: positionChanged(check.targetStart, target),
    pairAdjacentNow: Boolean(
      puller && target && puller.pos.isNearTo(target.pos)
    )
  };
}

pullerMoved and targetMoved tell you what changed; they do not prove that no other movement interaction affected the pair. If all calls returned OK but one or both positions did not change, continue with the movement progress diagnostic and inspect obstacles, competing movement, fatigue, and traffic rather than rewriting the pull return code.

Debug a pull pair in the right order

  1. Resolve both current-tick objects. Log IDs, names, positions, spawning state, and ownership before using saved state.
  2. Check adjacency. A range failure belongs before movement theory.
  3. Check the puller's movement capability. Inspect active MOVE parts and current fatigue.
  4. Log all three return values. Do not log only pull() and then guess which movement call failed.
  5. Keep the target's special move form exact. The target should call move(puller), not copy the puller's direction as if the two calls were ordinary independent movement.
  6. Inspect a later tick. Re-resolve both IDs and compare the positions you actually recorded.
  7. Only then investigate broader movement conflicts. Traffic, obstacles, competing intents, and route choice are downstream branches once the pair contract is known to have been scheduled correctly.

Keep pathfinding separate from pair movement

PathFinder.search() can choose the puller's next step, but it does not establish the pull relationship. Reject an incomplete search before converting its first complete path step into a direction:

function movePullPairToward(
  puller,
  target,
  destination,
  goalRange = 0
) {
  const search = PathFinder.search(
    puller.pos,
    { pos: destination, range: goalRange }
  );

  if (search.incomplete) {
    return { ok: false, reason: 'incomplete-path' };
  }

  if (search.path.length === 0) {
    return { ok: true, reason: 'goal-range-satisfied' };
  }

  const direction = puller.pos.getDirectionTo(search.path[0]);
  return schedulePullStep(puller, target, direction);
}

Pass the goal range that belongs to the downstream task; do not hard-code range 1 for every destination. The PathFinder.search() guide owns goal ranges, incomplete, partial paths, search costs, and search limits. This page only owns the two-Creep movement handoff after a valid next step exists.

Know what this guide does not own

This article covers one adjacent puller-target pair and one coordinated movement step. It does not promise a convoy algorithm, formation movement, cross-room persistence, traffic arbitration, or a fixed travel speed. Those systems need additional state, routing, collision policy, and measurement.

It also does not claim Screeps Console or live-shard execution for the examples above. The API contract was checked against current official documentation, the return-code edge was checked against the current public engine source, and the coordinator examples passed static syntax review plus deterministic local cases. Runtime pair movement remains a separate evidence level.

FAQ

Does puller.pull(target) move the target automatically?

No. Coordinate the puller's movement, pull(target), and target.move(puller) in the same tick.

Can the pulled target have fatigue or no active MOVE part?

The official special move(puller) form bypasses the target's ERR_TIRED and ERR_NO_BODYPART checks. The puller still has to make a valid movement step.

Should I expect pull() to return ERR_NO_BODYPART?

Do not build that assumption into your code. It is not listed in the current official pull() return table. Validate the puller's active MOVE part before scheduling the pair.

Why store the pair in Memory?

You only need persistent evidence when you want to compare the accepted current-tick calls with positions observed on a later tick. Do not store extra state when the result is not important to verify.

Official documentation

SOURCE AND SCOPE

Review the evidence or continue the system

This is an original English guide. Its verification record separates documentation-reviewed claims from Console or live-shard observations that have not been performed.