FOUNDATION · WORKING STATE

How to Switch a Screeps Creep Between Getting Energy and Working

Build a stable two-phase working state from Store boundaries, keep the previous state at partial Energy, handle initialization and invalid capacity, and separate harvesting from Controller upgrading.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — Memory, Store, harvest(), upgradeController(), game loop · Source correction: Current harvest() docs do not list ERR_FULL

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — Memory, Store, harvest(), upgradeController(), game loop
Source correction
Current harvest() docs do not list ERR_FULL
JavaScript syntax
Passed
Offline state review
Passed — empty, full, partial, first run, invalid capacity
Screeps Console test
Pending
Live multi-tick room test
Pending
Last verified
July 25, 2026

Quick answer

Use two Store boundaries to control a Creep's task. When carried Energy reaches zero, set creep.memory.working to false and acquire Energy. When free Energy capacity reaches zero, set it to true and spend Energy. At every partial value between empty and full, keep the previous state. This prevents the Creep from changing direction after every small harvest or every partial spend.

The two phases

working = false
→ acquire Energy

working = true
→ spend Energy on the assigned task

The word working is not an official Screeps state. It is a player-defined Memory field whose meaning comes from your own branches. Review Screeps Memory basics before using it across ticks.

Why you should not toggle every tick

This code creates task flapping:

creep.memory.working = !creep.memory.working;

The Creep may start moving toward a Source, reverse toward the Controller on the next tick, and reverse again before reaching either target. State should be driven by resource boundaries, not by the number of times the loop has run.

Read the Store boundaries

const usedEnergy = creep.store.getUsedCapacity(
  RESOURCE_ENERGY
);

const freeEnergyCapacity =
  creep.store.getFreeCapacity(RESOURCE_ENERGY);

const totalEnergyCapacity = creep.store.getCapacity(
  RESOURCE_ENERGY
);

The three values answer different questions:

ValueQuestionBoundary
usedEnergyHow much Energy is carried?0 means empty.
freeEnergyCapacityHow much more Energy fits?0 means full for Energy.
totalEnergyCapacityCan this Store carry Energy at all?0 is invalid for this cycle.

A Creep with no usable carrying capacity is not a valid acquisition-and-work worker. Check body design with the WORK, CARRY, and MOVE guide.

Calculate the next state with a pure function

function getNextWorkingState(input) {
  const {
    usedEnergy,
    freeEnergyCapacity,
    totalEnergyCapacity,
    previousWorking
  } = input;

  if (
    !Number.isFinite(usedEnergy)
    || !Number.isFinite(freeEnergyCapacity)
    || !Number.isFinite(totalEnergyCapacity)
    || usedEnergy < 0
    || freeEnergyCapacity < 0
    || totalEnergyCapacity <= 0
  ) {
    return {
      valid: false,
      working: false,
      reason: 'invalid-store-values'
    };
  }

  if (usedEnergy === 0) {
    return {
      valid: true,
      working: false,
      reason: 'energy-empty'
    };
  }

  if (freeEnergyCapacity === 0) {
    return {
      valid: true,
      working: true,
      reason: 'energy-full'
    };
  }

  return {
    valid: true,
    working: previousWorking === true,
    reason: 'keep-previous-state'
  };
}

The function does not depend on Screeps globals and does not modify Memory. That makes the state decision easy to test with ordinary numbers before connecting it to a live Creep.

Choose a first-run rule

Suppose a Creep has 20 Energy out of 50, but creep.memory.working has never been initialized. The official API does not define what your custom state should mean. This guide uses:

previousWorking === true

An undefined value therefore becomes false, so a partially loaded first-run Creep continues acquiring Energy. A repair emergency might choose the opposite policy. The important point is to make the initialization rule explicit rather than letting partial Energy produce inconsistent behavior.

Complete harvest-and-upgrade example

State impact: this script writes working, lastStateReason, and lastStateCheckedAt to Worker1 Memory. It may move the Creep, harvest from an active Source, or upgrade the owned room Controller.

function getNextWorkingState(input) {
  const {
    usedEnergy,
    freeEnergyCapacity,
    totalEnergyCapacity,
    previousWorking
  } = input;

  if (
    !Number.isFinite(usedEnergy)
    || !Number.isFinite(freeEnergyCapacity)
    || !Number.isFinite(totalEnergyCapacity)
    || usedEnergy < 0
    || freeEnergyCapacity < 0
    || totalEnergyCapacity <= 0
  ) {
    return {
      valid: false,
      working: false,
      reason: 'invalid-store-values'
    };
  }

  if (usedEnergy === 0) {
    return {
      valid: true,
      working: false,
      reason: 'energy-empty'
    };
  }

  if (freeEnergyCapacity === 0) {
    return {
      valid: true,
      working: true,
      reason: 'energy-full'
    };
  }

  return {
    valid: true,
    working: previousWorking === true,
    reason: 'keep-previous-state'
  };
}

function updateWorkingState(creep) {
  const decision = getNextWorkingState({
    usedEnergy: creep.store.getUsedCapacity(
      RESOURCE_ENERGY
    ),
    freeEnergyCapacity:
      creep.store.getFreeCapacity(
        RESOURCE_ENERGY
      ),
    totalEnergyCapacity: creep.store.getCapacity(
      RESOURCE_ENERGY
    ),
    previousWorking: creep.memory.working
  });

  creep.memory.working = decision.working;
  creep.memory.lastStateReason = decision.reason;
  creep.memory.lastStateCheckedAt = Game.time;

  return decision;
}

function moveToTarget(creep, target, range, label) {
  const moveResult = creep.moveTo(target, {
    range,
    reusePath: 10
  });

  if (
    moveResult !== OK
    && moveResult !== ERR_TIRED
  ) {
    console.log(JSON.stringify({
      type: 'worker-move-failed',
      creepName: creep.name,
      label,
      moveResult
    }));
  }

  return moveResult;
}

function runWorker(creep) {
  const state = updateWorkingState(creep);

  if (!state.valid) {
    return {
      status: 'invalid-working-state',
      reason: state.reason
    };
  }

  if (creep.memory.working !== true) {
    const source = creep.pos.findClosestByPath(
      FIND_SOURCES_ACTIVE
    );

    if (!source) {
      return {
        status: 'active-source-not-found'
      };
    }

    const harvestResult = creep.harvest(source);

    if (harvestResult === ERR_NOT_IN_RANGE) {
      const moveResult = moveToTarget(
        creep,
        source,
        1,
        'Source'
      );

      return {
        status: 'moving-to-source',
        harvestResult,
        moveResult
      };
    }

    return {
      status: harvestResult === OK
        ? 'harvest-submitted'
        : 'harvest-failed',
      harvestResult
    };
  }

  const controller = creep.room.controller;

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

  const upgradeResult =
    creep.upgradeController(controller);

  if (upgradeResult === ERR_NOT_IN_RANGE) {
    const moveResult = moveToTarget(
      creep,
      controller,
      3,
      'Controller'
    );

    return {
      status: 'moving-to-controller',
      upgradeResult,
      moveResult
    };
  }

  return {
    status: upgradeResult === OK
      ? 'upgrade-submitted'
      : 'upgrade-failed',
    upgradeResult
  };
}

module.exports.loop = function () {
  const creep = Game.creeps.Worker1;

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

  const outcome = runWorker(creep);

  if (
    outcome.status.endsWith('-failed')
    || outcome.status === 'invalid-working-state'
  ) {
    console.log(JSON.stringify({
      type: 'worker-state-action-failed',
      creepName: creep.name,
      working: creep.memory.working,
      ...outcome
    }));
  }
};

Replace Worker1 with the real Creep name. The example deliberately uses one acquisition target and one work target so the state rule remains visible.

Run only one action branch per tick

The acquisition branch returns before the work branch. This prevents the same loop from trying to harvest and upgrade after one state decision. Screeps processes actions after scripts run, and action pipelines can block one another even when multiple calls return OK. One chosen branch produces clearer movement and debugging.

Why partial Energy keeps the previous state

0 / 50 Energy
→ working = false

50 / 50 Energy
→ working = true

30 / 50 Energy
→ keep the previous state

A Creep already working with 30 Energy continues working until empty. A Creep already acquiring with 30 Energy continues filling until full. Switching merely because usedEnergy > 0 would make it leave the Source after the first successful harvest.

Important action results

ActionResults to handle firstMeaning in this example
harvest()OK, ERR_NOT_IN_RANGE, ERR_NOT_ENOUGH_RESOURCES, ERR_NO_BODYPARTAccepted, move closer, wait or reselect, or inspect WORK parts.
upgradeController()OK, ERR_NOT_IN_RANGE, ERR_NOT_ENOUGH_RESOURCES, ERR_INVALID_TARGETAccepted, move within range 3, return to acquisition, or inspect Controller state.
moveTo()OK, ERR_TIRED, ERR_NO_PATH, ERR_NO_BODYPARTMovement accepted, temporary fatigue, no route, or no active MOVE ability.

Source correction: the Chinese source included ERR_FULL in its harvesting table. The current official Creep.harvest() reference does not list that result. This English version prevents the full-Store case through the state boundary and does not claim that harvest() returns ERR_FULL.

Roles that should not use this pattern

  • A fixed upgrader that refills from a nearby Link while upgrading.
  • A dedicated hauler driven by a task queue.
  • A combat Creep.
  • A one-time temporary task.
  • A Creep whose actions are assigned by a centralized scheduler.

A state pattern is useful only when its two phases match the real job. Forcing a fixed upgrader to wait until completely full may reduce Controller throughput.

Debugging checklist

  • Do not invert working every tick.
  • Read used, free, and total Energy capacity separately.
  • Reject a total capacity of zero.
  • Choose an explicit first-run rule for partial Energy.
  • Keep the previous state between the empty and full boundaries.
  • Run only one action branch per tick.
  • Save the action result and movement result separately.
  • Confirm the work target belongs to the intended room and player.
  • Inspect lastStateReason when behavior looks wrong.
  • Use the error-code reference for less common results.

Scope and next steps

This article does not build a generic finite-state-machine framework, role dispatcher, task queue, target reservation system, fixed Link upgrader, or multi-room worker. Continue with restoring a saved target with Game.getObjectById().

Frequently asked questions

Why does my Creep reverse direction every tick?

The state is probably being toggled by tick or by a broad has-Energy test. Change state only at the empty and full boundaries.

What should happen on the first tick with partial Energy?

Choose a project rule. This guide defaults to acquisition mode until the Store is full.

Does working automatically perform an action?

No. It is only a custom boolean. Your branches must call the actual Screeps methods.

Should every upgrader use this pattern?

No. A fixed upgrader with local Link Energy may refill and upgrade continuously instead.

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.