Use the event and artifact contract
StructureSpawn.recycleCreep() is an irreversible retirement request. The call returning OK proves that the current script submitted an acceptable intent; it does not prove that the target disappeared until the server processes the tick.
The official engine then removes the exact Creep ID, creates a Tombstone at the Creep's position and appends an EVENT_OBJECT_DESTROYED record whose objectId is that Creep ID and whose type is creep. Because Room.getEventLog() reports the previous tick, the next script tick can match that exact event.
Resource recovery also follows the Creep position, not the Spawn position. The engine first fills a Container on the Creep tile when one exists and has capacity, then writes the remainder into the new Tombstone Store. Do not search for loose resource objects on the Spawn tile or infer a refund from Spawn Energy.
Bind exact Spawn and Creep identity
function buildRecycleConfirmation(request) {
return [
'RECYCLE_CREEP',
request.requestId,
request.spawnId,
request.creepId,
request.creepName
].join('_');
}
Memory.recycleRequests ??= {};
Memory.recycleRequests['retire-worker-17'] = {
requestId: 'retire-worker-17',
enabled: true,
spawnId: 'replace-with-spawn-id',
creepId: 'replace-with-creep-id',
creepName: 'OldWorker1',
homeRoomName: 'W1N1',
requestedAt: Game.time,
confirmation:
'RECYCLE_CREEP_retire-worker-17_' +
'replace-with-spawn-id_' +
'replace-with-creep-id_OldWorker1'
};
The name helps an operator identify the mission, but the retirement binds to IDs. A later Creep that reuses the same name is not the reviewed target.
Evaluate the current objects
function evaluateRecycleOperation(
request,
spawn,
creep
) {
if (!request || request.enabled !== true) {
return { ready: false, status: 'request-disabled' };
}
if (
typeof request.requestId !== 'string'
|| request.requestId.length === 0
|| typeof request.spawnId !== 'string'
|| typeof request.creepId !== 'string'
|| typeof request.creepName !== 'string'
|| typeof request.homeRoomName !== 'string'
|| !Number.isInteger(request.requestedAt)
) {
return { ready: false, status: 'request-invalid' };
}
if (request.confirmation !== buildRecycleConfirmation(request)) {
return { ready: false, status: 'confirmation-mismatch' };
}
if (!spawn || !creep) {
return { ready: false, status: 'object-unavailable' };
}
if (
spawn.id !== request.spawnId
|| creep.id !== request.creepId
|| creep.name !== request.creepName
) {
return { ready: false, status: 'operation-identity-mismatch' };
}
if (spawn.my !== true || creep.my !== true) {
return { ready: false, status: 'ownership-invalid' };
}
if (spawn.isActive() !== true) {
return { ready: false, status: 'spawn-inactive' };
}
if (creep.spawning === true) {
return { ready: false, status: 'creep-still-spawning' };
}
if (!spawn.pos.isNearTo(creep)) {
return { ready: false, status: 'move-to-spawn' };
}
return { ready: true, status: 'recycle-ready' };
}
There is intentionally no spawn.spawning rejection. The current API and processor do not require an idle Spawn for recycling.
Reserve both objects for the tick
function createRecycleDispatcher() {
const reservedSpawnIds = new Set();
const reservedCreepIds = new Set();
return {
reserve(spawnId, creepId) {
if (reservedSpawnIds.has(spawnId)) {
return {
ready: false,
status: 'spawn-already-reserved'
};
}
if (reservedCreepIds.has(creepId)) {
return {
ready: false,
status: 'creep-already-reserved'
};
}
reservedSpawnIds.add(spawnId);
reservedCreepIds.add(creepId);
return {
ready: true,
status: 'recycle-reserved'
};
},
release(spawnId, creepId) {
reservedSpawnIds.delete(spawnId);
reservedCreepIds.delete(creepId);
}
};
}
The Spawn reservation is a project coordination rule, not an engine busy rule. The Creep reservation prevents two adjacent Spawns or two retirement modules from targeting the same exact Creep during one script tick.
Submit once without automatic retry
function snapshotStore(store) {
return Object.fromEntries(
Object.keys(store).map(resourceType => [
resourceType,
store.getUsedCapacity(resourceType)
])
);
}
function submitRecycleOperation(
dispatcher,
requestId
) {
const request =
Memory.recycleRequests?.[requestId];
if (!request || request.requestId !== requestId) {
return { status: 'request-identity-mismatch' };
}
const spawn = typeof request.spawnId === 'string'
? Game.getObjectById(request.spawnId)
: null;
const creep = typeof request.creepId === 'string'
? Game.getObjectById(request.creepId)
: null;
const decision = evaluateRecycleOperation(
request,
spawn,
creep
);
if (decision.status === 'move-to-spawn' && creep && spawn) {
return {
status: 'moving-to-reviewed-spawn',
moveResult: creep.moveTo(spawn, {
range: 1,
reusePath: 10
})
};
}
if (!decision.ready) return decision;
const reservation = dispatcher.reserve(
spawn.id,
creep.id
);
if (!reservation.ready) return reservation;
Memory.pendingRecycleOperations ??= {};
if (Memory.pendingRecycleOperations[creep.id]) {
dispatcher.release(spawn.id, creep.id);
return { status: 'pending-operation-exists' };
}
const container = creep.room.lookForAt(
LOOK_STRUCTURES,
creep.pos.x,
creep.pos.y
).find(structure =>
structure.structureType === STRUCTURE_CONTAINER
) || null;
request.enabled = false;
request.lastAttemptAt = Game.time;
const before = {
ticksToLive: creep.ticksToLive,
body: creep.body.map(part => ({
type: part.type,
boost: part.boost || null
})),
creepRoomName: creep.pos.roomName,
creepX: creep.pos.x,
creepY: creep.pos.y,
containerId: container?.id ?? null,
containerStore: container
? snapshotStore(container.store)
: null
};
const result = spawn.recycleCreep(creep);
request.lastResult = result;
if (result !== OK) {
dispatcher.release(spawn.id, creep.id);
request.status =
'recycle-rejected-review-required';
return {
status: request.status,
result
};
}
Memory.pendingRecycleOperations[creep.id] = {
submittedAt: Game.time,
requestId: request.requestId,
spawnId: spawn.id,
creepId: creep.id,
creepName: creep.name,
before
};
request.status = 'accepted-awaiting-verification';
return {
status: request.status,
result,
spawnId: spawn.id,
creepId: creep.id
};
}
A rejected irreversible request remains disabled for human review. Movement may continue before submission, but the code never silently retries a rejected call and never falls back to creep.suicide().
Match the exact destruction event
function verifyRecycledCreep(pending) {
if (!pending) {
return { status: 'no-pending-operation' };
}
const expectedTick = pending.submittedAt + 1;
if (Game.time < expectedTick) {
return { status: 'waiting-for-next-tick' };
}
if (Game.time > expectedTick) {
return { status: 'verification-window-missed' };
}
const creep = Game.getObjectById(pending.creepId);
if (creep) {
return {
status: 'accepted-creep-still-observed',
creepId: creep.id
};
}
const room =
Game.rooms[pending.before.creepRoomName];
if (!room) {
return {
status: 'exact-creep-gone-room-evidence-unavailable'
};
}
const events = room.getEventLog().filter(event =>
event.event === EVENT_OBJECT_DESTROYED
&& event.objectId === pending.creepId
&& event.data?.type === 'creep'
);
const tombstones = room.find(FIND_TOMBSTONES).filter(
tombstone =>
tombstone.creep?.id === pending.creepId
&& tombstone.pos.x === pending.before.creepX
&& tombstone.pos.y === pending.before.creepY
);
if (events.length !== 1) {
return {
status: events.length === 0
? 'exact-creep-gone-destruction-event-not-observed'
: 'destruction-event-ambiguous',
eventCount: events.length,
tombstoneCount: tombstones.length
};
}
return {
status: tombstones.length === 1
? 'recycle-event-and-tombstone-observed'
: 'recycle-event-observed-artifact-mismatch',
eventCount: events.length,
tombstoneCount: tombstones.length,
tombstoneId: tombstones[0]?.id ?? null
};
}
The exact destruction event and exact Tombstone identity are stronger than name absence. A new Creep may reuse the old name, but it cannot reuse the destroyed object's ID.
Inspect the Creep-tile Tombstone and Container
function observeRecycleArtifacts(pending) {
const room =
Game.rooms[pending.before.creepRoomName];
if (!room) {
return { status: 'artifact-evidence-unavailable' };
}
const tombstone = room.find(FIND_TOMBSTONES).find(
item =>
item.creep?.id === pending.creepId
&& item.pos.x === pending.before.creepX
&& item.pos.y === pending.before.creepY
) || null;
const container = pending.before.containerId
? Game.getObjectById(pending.before.containerId)
: null;
const containerAfter = container
? snapshotStore(container.store)
: null;
return {
status: tombstone
? container
? 'recycle-artifacts-observed-container-confounded'
: 'recycle-tombstone-observed'
: 'recycle-tombstone-not-observed',
tombstoneId: tombstone?.id ?? null,
tombstoneStore: tombstone
? snapshotStore(tombstone.store)
: null,
containerId: container?.id ?? null,
containerBefore: pending.before.containerStore,
containerAfter
};
}
The Tombstone is matched by deceased Creep ID and Creep coordinates. A Container Store change is intentionally labelled confounded because transfers, withdrawals and other writes may affect the same Store. Report observed values instead of claiming a guaranteed refund from a net delta.
Keep incomplete evidence visible
| Status | Meaning | Response |
|---|---|---|
creep-already-reserved | Another retirement request owns this Creep for the tick | Do not submit a second call |
recycle-rejected-review-required | The API rejected the irreversible request | Keep it disabled and inspect the return code |
accepted-creep-still-observed | The saved ID still exists next tick | Preserve the discrepancy |
exact-creep-gone-destruction-event-not-observed | The exact ID vanished but the expected previous-tick event is absent | Keep the retirement unverified |
recycle-event-observed-artifact-mismatch | The exact event exists but no single matching Tombstone is visible | Inspect visibility and artifact identity |
recycle-artifacts-observed-container-confounded | The exact Tombstone and reviewed Container are visible | Report both Stores without forcing a refund delta |
Production integration boundary
Run retirement after replacement handoff and cargo-delivery checks. Verify accepted operations before cleaning custom indexes, and delete only Memory records that still identify the retired Creep ID. Live Console execution, exact recovered amounts, Container contention, simultaneous Spawn requests, genuine screenshots and CPU measurements remain pending.
Official documentation
Review the official Screeps API for StructureSpawn.recycleCreep(), Room.getEventLog(), EVENT_OBJECT_DESTROYED, FIND_TOMBSTONES, Tombstone.creep, Tombstone.store, Containers, range and return codes. The engine processor clarifies the exact Creep-tile artifact flow.