---
name: blender_animation_basics
description: "Animate an already-rigged mesh in Blender: set actions and keyframes, shape timing in the Dope Sheet, refine curves and easing in the Graph Editor, build motion cycles, layer clips in the NLA, and check the result with preview renders. Use when a finished rig needs motion, from a single posed action to a small set of layered clips."
metadata: { "includeInPrompt": true }
---

# Blender Animation Basics

## Purpose

Take a finished, tested rig and make it move: pose the control bones, set keyframes into named actions, shape the timing and the curves until the motion reads, build repeating cycles where the motion loops, layer finished actions in the NLA, and prove the result with preview renders. This skill covers animation only, from a rigged file to timed, checked motion.

Scope:
- blender-rigging-basics: "Rig a mesh in Blender: build an armature with clean bone layout and naming, bind the mesh, paint and clean weights, add constraints and IK, add control bones, and test deformation. Use when preparing a character or prop mesh so it can be posed and animated." That skill ends at a tested rig and does not cover animating the finished rig. This skill starts where it ends. If a pose deforms badly, that is a rigging fault: send it back instead of hiding it with keys.
- procedural-animation-canvas is 2D HTML Canvas code animation, and explainer-animation is an ffmpeg video pipeline for assembling finished footage. Neither animates a 3D rig in Blender. This skill owns 3D keyframe animation inside Blender: actions, F-Curves, the Dope Sheet, the Graph Editor, motion cycles, the NLA, and preview renders.
- This skill does not cover rigging, modeling, materials, final lighting, or final quality rendering. A preview render here is a motion check at low settings, not a delivered frame.
- Tool claims in this skill were checked against Blender 5.1.1 on 2026-10-15 by running Blender headless (`blender -b --factory-startup -noaudio --python script.py`). Operator names quoted below were checked against the actual operator lists of that build, because a name that merely looks right can still not exist. Property names, enum values, and the audit numbers were produced by real runs, not recalled. Blender changes operator and property names between major versions, and 5.1.1 uses layered actions, so re-check a quoted name in the Python console of a newer build before relying on it.

## Workflow

1. Take over the rig and set the scene before posing anything.
   - Open the rigged file and read its rig summary first: which bones are controls, which chains are IK-driven, and which joints have known weak spots. Key the control bones and the FK chains the summary names. Keying an IK-driven deform bone directly fights the solver and produces keys that appear to do nothing.
   - Save a working copy under a new name before the first key. Animation edits are hard to peel out of a file once actions, NLA strips, and baked curves mix together.
   - Set the scene frame range and frame rate before timing anything: `frame_start` and `frame_end` on the scene, and `fps` on `scene.render`. Timing decisions are frame counts, and a frame rate changed halfway through retimes every action in the file. The audit script in the Operating Rules section prints both values at the top of its output.
   - Set the preview range deliberately if you use one. The scene properties `use_preview_range`, `frame_preview_start`, and `frame_preview_end` exist in 5.1.1. A preview range left on from an earlier task makes playback and preview renders stop early and look like missing animation. The audit prints `use_preview_range` so the state is on record.
   - Put timeline markers on the beats you already know, such as contact, apex, and landing. `scene.timeline_markers.new(name, frame)` works headless in 5.1.1, and markers give the timing sheet in the output contract something fixed to refer to.

2. Plan the motion in frames before touching the rig.
   - Write the beats in order with a frame for each: start pose, anticipation, main move, settle, end pose. Convert seconds to frames with the scene fps (at 24 fps, 12 frames is half a second, 6 frames is a quarter second). A beat with no frame number is a beat that will drift during posing.
   - Decide what loops and what plays once. A walk, an idle, and a machine cycle are loop material (step 8). A jump, a pickup, and a reaction are one-shot actions. Mixing the two in one action makes both harder to reuse in the NLA.
   - Decide which bones each beat needs. Most beats move a handful of controls: the root or hips, one limb chain, the spine, the head. If a beat seems to need every bone keyed, the beat is probably two beats.
   - Note the poses that must read from the camera view, not from the rig's front view. A pose that reads in the viewport orbit view can collapse from the render camera. The preview render in step 11 is the check, but planning for the camera now saves rework.

3. Create one named action per clip and protect it.
   - An action is the container for one clip's keys: one walk, one idle, one jump. Create actions in the Dope Sheet action editor (`bpy.ops.action.new` and `bpy.ops.action.duplicate` exist in 5.1.1) or by keying with a fresh action assigned. In Python, `bpy.data.actions.new(name)` creates one and assigning it to `rig.animation_data.action` makes it active; both were run in 5.1.1 for the audit fixture.
   - Name actions for their motion and owner, for example `Bounce`, `Idle`, or `WalkCycle`, never for their version. `Action.001` in a delivered file means nobody can tell what plays where, and the NLA layout in the output contract is built from these names.
   - Set `use_fake_user` on every action you intend to keep. The property exists on the Action data block in 5.1.1. An action with no users and no fake user is dropped when the file is saved and reloaded, which is the standard way finished clips quietly vanish from a project.
   - Keep one action active at a time while keying. Keys always land in the active action of the object being keyed. If keys appear in the wrong clip, the active action was wrong, and the fix is moving the keys, not re-posing.
   - Actions in 5.1.1 are layered: an action holds layers, strips, and channelbags, and the F-Curves live in the channelbags. Scripts written for older Blender versions that read `action.fcurves` directly will fail or read nothing on a layered action. The audit script shows the working read path: layers, then strips, then channelbags, then `fcurves`, with the legacy `action.fcurves` collection used only when `action.is_action_layered` is false.

4. Pose and insert keyframes.
   - Work in Pose Mode on the armature. Pose a control bone, then insert keys for that pose before moving to the next beat. In the UI the Insert Keyframe menu is `bpy.ops.anim.keyframe_insert_menu`; in Python, `keyframe_insert(data_path, frame)` on the pose bone or object inserts the key and returns True on success, verified in 5.1.1. Scripted inserts work headless; most editor operators in steps 5 and 6 need their editor open and are UI steps.
   - Key only the channels a beat changes. `keyframe_insert` with no `index` keys every component of the property (a location key writes X, Y, and Z curves); passing `index` keys one component only. The audit fixture keys `rotation_euler` with `index=0` to animate one rotation axis without creating flat curves for the other two. Flat curves for channels that never move are noise the Graph Editor pass has to wade through.
   - Default new keys in 5.1.1 are BEZIER interpolation with AUTO_CLAMPED handles: the user preferences `keyframe_new_interpolation_type` reads BEZIER and `keyframe_new_handle_type` reads AUTO_CLAMPED on a factory startup. Blocking passes often want LINEAR or CONSTANT instead (step 5). Set the preference before a blocking pass instead of fixing every key after.
   - Know what the keying settings do before turning them on. Tool settings in 5.1.1 include `use_keyframe_insert_auto` (auto-keying), `auto_keying_mode`, `use_keyframe_insert_keyingset`, and `use_keyframe_cycle_aware`. Auto-keying while exploring poses fills actions with keys nobody chose. Keep it off during blocking unless the pass is specifically a capture pass.
   - The preference `use_visual_keying` exists in 5.1.1 and matters on constrained and IK-driven bones: visual keying keys the result the rig actually shows instead of the raw channel value. If a constrained bone plays back differently from the pose you set, check this setting before re-posing.
   - Build in-betweens with the pose tools instead of guessing halfway poses by hand. `bpy.ops.pose.breakdown`, `bpy.ops.pose.push`, `bpy.ops.pose.relax`, `bpy.ops.pose.propagate`, and `bpy.ops.pose.blend_to_neighbor` all exist in 5.1.1 and operate on the selected pose bones between their neighbouring keys.
   - Use motion paths as a spacing check on fast movers such as hands and feet. `bpy.ops.pose.paths_calculate` and `bpy.ops.object.paths_calculate` exist in 5.1.1. Even spacing along the path reads as even motion; bunched points read as a slow patch, whatever the curves say.

5. Block the timing in the Dope Sheet.
   - The Dope Sheet is where keys move in time. Do the timing pass here before touching curves: slide whole poses earlier or later until the beats land on the frames planned in step 2. Timing fixed in the Dope Sheet is cheap; timing fixed by dragging curve handles is slow and damages the curve shapes.
   - Mark key types as you block. Keyframe points carry a `type` property with values KEYFRAME, BREAKDOWN, MOVING_HOLD, EXTREME, JITTER, and GENERATED in 5.1.1, set in the editor with `bpy.ops.action.keyframe_type`. Breakdowns and extremes marked during blocking tell the next editor which keys are structural and which are disposable.
   - During blocking, hold poses with CONSTANT interpolation and move with LINEAR. CONSTANT keys make each pose read at full value until the next pose, so the blocking pass judges poses and timing only, with no accidental ease hiding a weak pose. Convert to BEZIER in step 6 once the timing is right.
   - Jump between keys instead of scrubbing blind. `bpy.ops.screen.keyframe_jump` and `bpy.ops.screen.frame_jump` exist in 5.1.1, and `bpy.ops.screen.animation_play` starts and stops playback. Judge blocking at full playback speed, not by dragging the playhead, because slow scrubbing forgives timing that real playback exposes.
   - Mirror or offset a blocked half when the motion is symmetric, instead of posing the second half from scratch. `bpy.ops.action.mirror` and `bpy.ops.graph.mirror` exist in 5.1.1. Mirroring copies timing errors as faithfully as it copies good timing, so mirror only after the first half plays well on its own.
   - Watch for two timing faults at this stage: keys on non-integer frames (usually left by a scaled or slid selection) and keys outside the scene frame range (usually an action that grew past the shot). Both are invisible in a quick playthrough and both break later steps. The audit counts both, per action.

6. Refine the curves in the Graph Editor.
   - Each animated channel is one F-Curve: a `data_path` plus an `array_index`, holding keyframe points with a frame and a value (`co`), an interpolation mode, an easing, and two handles. Read curves this way when scripting; the audit lists data paths per action so you can confirm the curves that exist are the curves the motion needs.
   - Interpolation is a per-key property, and it governs the segment that starts at that key. The values in 5.1.1 are CONSTANT, LINEAR, BEZIER, SINE, QUAD, CUBIC, QUART, QUINT, EXPO, CIRC, BACK, BOUNCE, and ELASTIC. In the editor, `bpy.ops.graph.interpolation_type` sets it on selected keys and `bpy.ops.action.interpolation_type` does the same from the Dope Sheet side; both exist in 5.1.1.
   - Use BEZIER for organic moves, LINEAR for mechanical moves at constant speed, and CONSTANT for holds and stepped mechanical motion. A walk cycle with every key on BEZIER drifts and floats; a machine part on BEZIER surges at each end of its travel. Mixed interpolation inside one action is normal and expected.
   - Handles decide the shape of a BEZIER segment. Handle types in 5.1.1 are FREE, ALIGNED, VECTOR, AUTO, and AUTO_CLAMPED, set with `bpy.ops.graph.handle_type`. AUTO_CLAMPED is the safe default for body motion because it resists overshoot at keys. Overshoot past a key value on a location curve is the foot going through the floor or the hand passing the prop it was reaching for.
   - Set what happens outside a curve's keys with the F-Curve `extrapolation` property: CONSTANT holds the end value, LINEAR continues the end slope. In the editor this is `bpy.ops.graph.extrapolation_type`. LINEAR extrapolation on a curve whose last segment still moves will run away during an NLA hold or a long shot, so it needs a deliberate reason.
   - Clean curves mechanically before shaping them by eye. `bpy.ops.graph.clean` exists in 5.1.1 and takes a `threshold` value; it removes keys that add no shape, which is the standard state of a curve after auto-keying or a bake. `bpy.ops.action.clean` does the same job from the action side. Clean first, then shape, because shaping a curve that still carries junk keys wastes the shaping.
   - Rotation curves get one extra check: the Euler filter. `bpy.ops.graph.euler_filter` exists in 5.1.1 and fixes the frame where a rotation jumps by a full turn between neighbouring keys, which shows up in playback as a limb spinning once for no reason.
   - Smooth a noisy curve only when the noise is the problem. `bpy.ops.graph.smooth`, `bpy.ops.graph.gaussian_smooth`, and `bpy.ops.graph.butterworth_smooth` exist in 5.1.1. Smoothing a curve that was keyed deliberately flattens the accents the animator put in, so smooth baked or captured curves, not hand-keyed ones.

7. Set easing deliberately, per segment.
   - Easing applies to the dynamic interpolation types (SINE through ELASTIC) through the keyframe `easing` property: AUTO, EASE_IN, EASE_OUT, and EASE_IN_OUT in 5.1.1. In the editor, `bpy.ops.graph.easing_type` sets it on selected keys. AUTO is a starting point, not a decision.
   - Match easing to the physical story: a falling object eases in (it accelerates), a thrown object eases out at the throw and in on the way down, a heavy door eases in and out. A segment eased the wrong way reads as the object being pulled by a string, and viewers spot it without knowing why.
   - BACK, BOUNCE, and ELASTIC carry extra properties on the keyframe point: `back` for the overshoot amount on BACK, and `amplitude` and `period` for BOUNCE and ELASTIC. All three exist on the Keyframe type in 5.1.1. These are accent tools for one segment, such as a landing squash or a springy antenna. A whole action built on them reads as rubber, and they never substitute for correctly timed keys.
   - Prefer a moving hold over a dead hold. A pose held for more than a few frames should still drift slightly, through one low, wide BEZIER curve on the root or chest, rather than sitting at a frozen value. Frozen holds read as the playback having stopped.
   - Check easing at the joins between actions, not only inside them. The last segment of one clip and the first segment of the next decide whether an NLA cut or blend feels like a hitch. Step 9 handles the blend settings; this step owns the curve shapes that lead into and out of the join.

8. Build motion cycles that actually loop.
   - A loop has one hard requirement: the value and the slope at the last key must continue into the first key. Pose the loop end as a copy of the loop start one cycle later, then remove the duplicated end key if the cycle mechanism supplies the wrap, or keep it if the action plays standalone. Decide which convention the action uses and write it in the action list, because the NLA repeat count in step 9 depends on it.
   - Route one for a cycle: an F-Curve Cycles modifier. Add it with `fc.modifiers.new(type="CYCLES")` in Python or `bpy.ops.graph.fmodifier_add` in the editor, both present in 5.1.1. Its `mode_before` and `mode_after` properties take NONE, REPEAT, REPEAT_OFFSET, and MIRROR. REPEAT replays the keyed range as-is (right for a walk), REPEAT_OFFSET adds the cycle's net change each repeat (right for a channel that must keep travelling), and MIRROR plays the range forward then backward (right for a sway or a bob built from half a cycle).
   - The Cycles modifier must be the first modifier on its curve. Verified in 5.1.1: adding a Cycles modifier to a curve that already carries another modifier fails with an error stating it can only be first in the stack. If a curve needs a Cycles modifier added late, remove the other modifiers, add Cycles, then re-add the rest after it.
   - Route two for a cycle: the action itself is the loop and the NLA repeats it (step 9). The Action property `use_cyclic` exists in 5.1.1 and flags the action as cyclic. Use one route per cycle, not both: a Cycles modifier inside an action that the NLA also repeats produces a loop inside a loop and a seam nobody can locate.
   - Test the seam specifically. Play or render the frames around the wrap point (last frames into first frames) at full speed. A seam shows as a one-frame pop or a speed change at the join. Fix order: check the end pose truly matches the start pose, then check the end segment's slope against the first segment's slope, then check for a duplicated or missing key at the join.
   - Keep cycle length in whole frames. A cycle whose last key sits on a fractional frame cannot repeat cleanly in the NLA, and the audit's non-integer keyframe count will show it.

9. Layer finished actions in the NLA.
   - The NLA stacks actions as strips on tracks so clips can be sequenced, repeated, and blended without merging their keys. An object's `animation_data` holds `nla_tracks`; each track holds strips; each strip points at an action. In Python, `nla_tracks.new()` creates a track and `track.strips.new(name, start, action)` creates a strip; both were run in 5.1.1 for the audit fixture. In the editor, `bpy.ops.nla.tracks_add` and `bpy.ops.nla.actionclip_add` exist.
   - Move an action into the NLA with push down or stash instead of re-keying it. `bpy.ops.action.push_down` and `bpy.ops.nla.action_pushdown` push the active action down into a track, and `bpy.ops.action.stash` stashes it as a muted strip for later use; all exist in 5.1.1. After a push down, the pushed action no longer drives the object directly, which surprises people mid-task: check the active action state after every push down.
   - Set strip timing on the strip, not by editing the action. Strip properties in 5.1.1 include `frame_start` and `frame_end` (where the strip sits and how long it plays), `action_frame_start` and `action_frame_end` (which part of the action it plays), `repeat` (how many times the action cycles inside the strip), and `scale` (the strip's time stretch). Verified in the fixture run: a strip placed at frame 1 over an action whose keys run 1 to 60, with `repeat` set to 2.0, reported strip frames 1 to 119.
   - Set how strips combine deliberately. Strip `blend_type` values in 5.1.1 are REPLACE, COMBINE, ADD, SUBTRACT, and MULTIPLY, and `extrapolation` values are NOTHING, HOLD, and HOLD_FORWARD. REPLACE on the bottom track is the base motion. ADD suits an offset layer, such as a breathing cycle added over an idle. HOLD_FORWARD keeps the last pose after the strip ends; NOTHING lets the character snap back, which is right only when a later strip takes over on the next frame.
   - Use `blend_in` and `blend_out` on the strip for joins between clips instead of keying a manual transition, and `use_auto_blend` when the overlap of two strips should set the blend. A join with no blend and mismatched end poses pops exactly like a bad loop seam, and the fix is the same order: match the poses first, then set the blend.
   - Mute and solo are track properties (`mute` and `is_solo` on the track, `mute` on the strip) in 5.1.1. Debug layering problems by soloing one track at a time and playing it, rather than deleting strips to see what changes. What each layer contributes should be statable in one sentence per track in the NLA layout section of the output.
   - Tweak mode edits one strip's action in place: `bpy.ops.nla.tweakmode_enter` and `bpy.ops.nla.tweakmode_exit` exist in 5.1.1. Keys inserted outside tweak mode while NLA tracks are driving the object land somewhere other than the strip you are looking at, which is the most common cause of "my edit did nothing" in NLA work. Check whether tweak mode is on before keying an object that has strips.

10. Bake only when a downstream step needs baked keys.
    - Baking converts evaluated motion (constraints, IK, NLA layering) into plain per-frame keys. `bpy.ops.nla.bake` exists in 5.1.1 and takes `frame_start`, `frame_end`, `step`, `only_selected`, `visual_keying`, `clear_constraints`, `clear_parents`, `use_current_action`, `clean_curves`, `bake_types`, and `channel_types`. Use it when handing motion to a target that cannot evaluate the rig or the NLA stack, or when a segment must be frozen exactly as it plays now.
    - Bake a copy, never the only version. Keep the unbaked file with its actions and strips, and bake into a separate file or a clearly named baked action. A baked action at step 1 has one key per frame per channel; hand edits after that are curve surgery, and `bpy.ops.graph.clean` with a threshold is the tool that thins it back out.
    - A baked curve can be turned into samples and back with `bpy.ops.graph.keys_to_samples` and `bpy.ops.graph.samples_to_keys`, both in 5.1.1. Samples suit noise and audio-driven curves; keys suit anything a person will edit later.
    - After a bake, run the audit and compare: the baked action should cover the same frame range, and its keyframe count should be roughly the frame count times the channel count at step 1. A baked action that is short on keys was baked over the wrong range.

11. Check the motion with preview renders.
    - Playback in a solid viewport hides surface, weight, and silhouette problems that a rendered frame shows. Render single preview frames at the key poses and at the midpoints between them, where bad curve shapes do their damage.
    - A preview render needs a camera, or it fails outright. Verified in 5.1.1: `bpy.ops.render.render` on a scene with no camera errors with "Cannot render, no camera" and writes nothing. If a file arrives with no camera, add a simple one for checking and say so in the summary, rather than skipping the render check.
    - Keep preview settings cheap and fixed: Cycles on CPU, `scene.cycles.samples` at 1, a small resolution such as 64 by 36 for a contact check or a few hundred pixels wide for a pose check, and the same settings for every frame in the pass so frames are comparable. Verified run on 5.1.1 headless: with a camera and a point light added, engine CYCLES, device CPU, 1 sample, 64 by 36, `bpy.ops.render.render(write_still=True)` at frame 13 wrote a 3.1 KB PNG. EEVEE is not used for headless checks here because it fails in this headless environment.
    - Set `scene.frame_set(frame)` before each still, set `scene.render.filepath` to a folder and name you will actually look in, and confirm the output file exists and is non-empty after the render call. A render call that returns without an error and without a file is a failed check, not a passed one. Note that the file written may not carry the frame number in its name, so check the actual output path the render reports.
    - A viewport render (`bpy.ops.render.opengl` exists in 5.1.1) is a valid second check from inside the UI, where a viewport exists. Do not script it headless; it needs that viewport context.
    - Read the preview frames for motion faults, in this order: feet or props sliding against the ground (timing or root curve), a limb popping at a join (a seam or a missing blend), a pose that reads from the front but not from the camera (step 2 planning), and holds that look frozen (step 7). Record each fault with its frame numbers in the preview evidence section.

12. Clean up and run the audit before delivery.
    - Delete keys and curves that carry no motion: flat curves from full-property inserts (step 4), junk keys from auto-keying, and actions left over from abandoned takes. An action you keep gets a real name and `use_fake_user`; an action you do not keep gets deleted, not renamed to `old`.
    - Confirm every action's keys sit inside its intended range and on whole frames, unless a non-integer key is deliberate and noted. Confirm each loop action uses exactly one cycle route (step 8). Confirm the NLA stack plays the shot end to end with no snap at a strip join.
    - Run the audit script from the Operating Rules section on the final file and put its full output in the animation summary. The audit proves the mechanical state: ranges, counts, interpolation mix, modifiers, NLA layout. It does not judge whether the motion is good; the preview frames in step 11 are the evidence for that.
    - Fix audit findings in this order: keys outside the scene range (they retime or vanish in the NLA), non-integer keys in a loop, an action with no fake user that the summary claims will survive, and only then cosmetic naming.

## Output Contract

Deliver the animated Blender file plus an animation summary with these sections, in this order:

1. Scene summary: Blender version used, rig and animated object names, scene frame range, fps, preview range state, and what motion was requested.
2. Audit output: the full output of anim_audit.py on the final file. On the worked fixture in the Operating Rules section the headline numbers are: scene range 1 to 48, fps 24, markers 2, actions 2, animated objects 1. Bounce: fcurves 4, keyframes 12, first key 1, last key 60, interpolations BEZIER 10 and LINEAR 2, keyframe types BREAKDOWN 1 and KEYFRAME 11, modifiers 1 (a CYCLES modifier), keys outside the scene range 1, non-integer keyframes 0. Idle: fcurves 1, keyframes 3, interpolations CONSTANT 3, non-integer keyframes 1. NLA: 1 track holding BounceStrip over frames 1 to 119 with repeat 2. A delivered shot should show 0 keys outside the scene range and 0 non-integer keyframes in any looping action, except where the summary names the exception and its reason.
3. Action list: one row per action with its name, what motion it holds, its first and last key frames, whether it loops and by which route (Cycles modifier or NLA repeat), and whether `use_fake_user` is set.
4. Timing sheet: one row per beat with its marker name if one exists, its frame, and the pose or event that happens there, matching the plan from Workflow step 2 as built, not as first planned.
5. NLA layout: one row per track and strip with the strip name, its action, strip frames, the action frames it plays, repeat, scale, blend type, extrapolation, and blend in and out. One sentence per track stating what that layer contributes.
6. Preview evidence: the frames rendered, the render settings used (engine, device, samples, resolution), the output file paths checked, and the faults found per frame with the fix applied or the reason it was accepted.
7. Limits: motion the file does not contain, actions left unbaked that a downstream step may need baked, known weak poses or joins, and anything deferred, such as facial animation or a longer cycle.

## Operating Rules

1. Keys land in the active action only. Before any keying pass, state the active action out loud in the summary or the work notes. A pass keyed into the wrong action is rework, and it is prevented by this one check.
2. One action per clip, one cycle route per loop. An action holds one piece of motion, and a loop repeats by a Cycles modifier or by NLA repeat, never both. Violations produce seams and doubled motion that no later pass can cleanly separate.
3. Timing before curves. No Graph Editor shaping happens on an action whose Dope Sheet timing is still wrong. Shaping first locks bad timing inside good-looking curves and doubles the fix cost.
4. No silent out-of-range or fractional keys. Keys outside the scene frame range and keys on non-integer frames appear in the audit output and are either fixed or named as deliberate in the animation summary. A loop with a fractional key does not ship.
5. Control bones get the keys. Deform bones and IK-driven bones are not keyed directly, per the rig summary handed over from rigging. If a shot truly needs it, the exception and its reason go in the limits section.
6. A Cycles modifier goes first on its curve. This is mechanical, verified in 5.1.1: Blender refuses to add it after another modifier. Reorder by removing and re-adding modifiers, never by stacking attempts.
7. Absolute rules in this skill are the active action check, fake user on kept actions, one cycle route per loop, and whole frames for loop keys, because breaking any of them loses or corrupts motion mechanically. Interpolation choices, easing choices, cycle length, blend lengths, and preview resolution are guidance: choose for the shot and record the choice in the summary.
8. No invented Blender calls. Every `bpy.ops` name and property quoted in this skill was checked in Blender 5.1.1 as described in the Purpose section. Operator existence was checked against the build's operator lists, not by attribute access, which reports names that do not exist. When working in a different version, confirm a name in that build before scripting with it, and soften any instruction that depends on a name you could not confirm.
9. Use the animation audit script below for the mechanical checks. Save it as anim_audit.py, run it inside Blender as its header describes, and put its output in the animation summary. Report numbers from its output, not from memory of the file.

### Animation audit (anim_audit.py)

Run: `blender -b --factory-startup -noaudio --python anim_audit.py -- --make-fixture` reproduces the worked example. Run it on a real file with `blender -b your_file.blend -noaudio --python anim_audit.py`. On the default factory scene it prints the scene range 1 to 250, fps 24, and zero actions, markers, and animated objects.

Expected output for the fixture run:

```text
scene_frame_range: 1-48
fps: 24
use_preview_range: False
markers: 2: apex@13, landing@25
actions: 2
action: Bounce
  layered: True
  use_cyclic: False
  fcurves: 4
  data_paths: pose.bones["root"].location, pose.bones["spine"].rotation_euler
  keyframes: 12
  first_key: 1
  last_key: 60
  interpolations: BEZIER: 10, LINEAR: 2
  keyframe_types: BREAKDOWN: 1, KEYFRAME: 11
  modifiers: 1: pose.bones["root"].location[2]: CYCLES
  keys_outside_scene_range: 1
  non_integer_keyframes: 0
action: Idle
  layered: True
  use_cyclic: False
  fcurves: 1
  data_paths: pose.bones["arm.L"].location
  keyframes: 3
  first_key: 1
  last_key: 25
  interpolations: CONSTANT: 3
  keyframe_types: KEYFRAME: 3
  modifiers: 0: -
  keys_outside_scene_range: 0
  non_integer_keyframes: 1
animated_objects: 1
object: AnimRig
  active_action: Bounce
  use_nla: True
  nla_tracks: 1
  track: NlaTrack: mute=False
    strip: BounceStrip: action=Bounce frames=1-119 repeat=2 scale=1 blend=REPLACE extrapolation=HOLD mute=False
```

The fixture is built to trip each check once: Bounce carries one key at frame 60, past the scene end of 48, and one BREAKDOWN key at frame 13; its spine curve is LINEAR while its root curves stay BEZIER; its root Z curve carries the single CYCLES modifier. Idle carries one key at frame 12.5, the non-integer case, with all keys set to CONSTANT. The BounceStrip repeats Bounce twice, which is why its strip frames run 1 to 119 over an action whose keys span 1 to 60.

anim_audit.py (run inside Blender 5.1.1, uses only bpy and sys). Save this code as anim_audit.py:


```python
#!/usr/bin/env python3
"""Audit animation in the current Blender scene. Blender 5.1.1.

Run inside Blender, not with system python:
  blender -b --factory-startup -noaudio --python anim_audit.py
  blender -b your_file.blend -noaudio --python anim_audit.py
Reproduce the worked example in the skill (builds a fixture first):
  blender -b --factory-startup -noaudio --python anim_audit.py -- --make-fixture

What it reports: scene frame range and fps, timeline markers, every
action with its F-Curves, keyframe counts, interpolation and keyframe
type counts, F-Curve modifiers, keys outside the scene range and keys
on non-integer frames, and per animated object its active action and
NLA tracks and strips. F-Curves are read through the layered action
channelbags when the action is layered, and through the legacy
fcurves collection when it is not.
"""
import sys

import bpy


def fmt(v):
    return f"{v:g}"


def make_fixture():
    bpy.ops.object.select_all(action="SELECT")
    bpy.ops.object.delete()
    scene = bpy.context.scene
    scene.frame_start = 1
    scene.frame_end = 48
    scene.render.fps = 24
    arm_data = bpy.data.armatures.new("AnimRigData")
    rig = bpy.data.objects.new("AnimRig", arm_data)
    bpy.context.collection.objects.link(rig)
    bpy.context.view_layer.objects.active = rig
    bpy.ops.object.mode_set(mode="EDIT")
    specs = [
        ("root", (0, 0, 0), (0, 0, 1), None),
        ("spine", (0, 0, 1), (0, 0, 2), "root"),
        ("arm.L", (0, 0, 2), (1, 0, 2), "spine"),
    ]
    made = {}
    for name, head, tail, parent in specs:
        eb = arm_data.edit_bones.new(name)
        eb.head = head
        eb.tail = tail
        made[name] = eb
    for name, head, tail, parent in specs:
        if parent:
            made[name].parent = made[parent]
    bpy.ops.object.mode_set(mode="POSE")
    root = rig.pose.bones["root"]
    for frame, z in [(1, 0.0), (13, 0.5), (25, 0.0)]:
        root.location = (0, 0, z)
        root.keyframe_insert(data_path="location", frame=frame)
    spine = rig.pose.bones["spine"]
    for frame, ang in [(1, 0.0), (25, 0.261799)]:
        spine.rotation_euler = (ang, 0, 0)
        spine.keyframe_insert(data_path="rotation_euler", frame=frame, index=0)
    bounce = rig.animation_data.action
    bounce.name = "Bounce"
    for fc in fcurves_of(bounce):
        if fc.data_path == 'pose.bones["spine"].rotation_euler':
            for kp in fc.keyframe_points:
                kp.interpolation = "LINEAR"
            fc.update()
        if fc.data_path == 'pose.bones["root"].location' and fc.array_index == 2:
            for kp in fc.keyframe_points:
                if kp.co.x == 13.0:
                    kp.type = "BREAKDOWN"
            mod = fc.modifiers.new(type="CYCLES")
            mod.mode_before = "REPEAT"
            mod.mode_after = "REPEAT"
    root.location = (0, 0, 0.0)
    root.keyframe_insert(data_path="location", frame=60, index=2)
    idle = bpy.data.actions.new("Idle")
    rig.animation_data.action = idle
    arm = rig.pose.bones["arm.L"]
    for frame, x in [(1, 0.0), (12.5, 0.05), (25, 0.1)]:
        arm.location = (x, 0, 0)
        arm.keyframe_insert(data_path="location", frame=frame, index=0)
    for fc in fcurves_of(idle):
        for kp in fc.keyframe_points:
            kp.interpolation = "CONSTANT"
        fc.update()
    rig.animation_data.action = bounce
    track = rig.animation_data.nla_tracks.new()
    strip = track.strips.new(name="BounceStrip", start=1, action=bounce)
    strip.repeat = 2.0
    scene.timeline_markers.new(name="apex", frame=13)
    scene.timeline_markers.new(name="landing", frame=25)
    return rig


def fcurves_of(action):
    out = []
    if action.is_action_layered:
        for layer in action.layers:
            for strip in layer.strips:
                for channelbag in strip.channelbags:
                    out.extend(list(channelbag.fcurves))
    else:
        out.extend(list(action.fcurves))
    return sorted(out, key=lambda f: (f.data_path, f.array_index))


def audit():
    scene = bpy.context.scene
    print(f"scene_frame_range: {scene.frame_start}-{scene.frame_end}")
    print(f"fps: {scene.render.fps}")
    print(f"use_preview_range: {scene.use_preview_range}")
    markers = sorted(scene.timeline_markers, key=lambda m: (m.frame, m.name))
    print(f"markers: {len(markers)}: {', '.join(f'{m.name}@{m.frame}' for m in markers) if markers else '-'}")
    actions = sorted(bpy.data.actions, key=lambda a: a.name)
    print(f"actions: {len(actions)}")
    for action in actions:
        fcurves = fcurves_of(action)
        total_keys = 0
        outside = 0
        non_integer = 0
        interp_counts = {}
        type_counts = {}
        mods = []
        frames = []
        for fc in fcurves:
            for kp in fc.keyframe_points:
                total_keys += 1
                frames.append(kp.co.x)
                interp_counts[kp.interpolation] = interp_counts.get(kp.interpolation, 0) + 1
                type_counts[kp.type] = type_counts.get(kp.type, 0) + 1
                if kp.co.x < scene.frame_start or kp.co.x > scene.frame_end:
                    outside += 1
                if abs(kp.co.x - round(kp.co.x)) > 0.001:
                    non_integer += 1
            for mod in fc.modifiers:
                mods.append(f"{fc.data_path}[{fc.array_index}]: {mod.type}")
        print(f"action: {action.name}")
        print(f"  layered: {action.is_action_layered}")
        print(f"  use_cyclic: {action.use_cyclic}")
        print(f"  fcurves: {len(fcurves)}")
        print(f"  data_paths: {', '.join(sorted({fc.data_path for fc in fcurves})) if fcurves else '-'}")
        print(f"  keyframes: {total_keys}")
        print(f"  first_key: {fmt(min(frames)) if frames else '-'}")
        print(f"  last_key: {fmt(max(frames)) if frames else '-'}")
        print(f"  interpolations: {', '.join(f'{k}: {v}' for k, v in sorted(interp_counts.items())) if interp_counts else '-'}")
        print(f"  keyframe_types: {', '.join(f'{k}: {v}' for k, v in sorted(type_counts.items())) if type_counts else '-'}")
        print(f"  modifiers: {len(mods)}: {', '.join(sorted(mods)) if mods else '-'}")
        print(f"  keys_outside_scene_range: {outside}")
        print(f"  non_integer_keyframes: {non_integer}")
    animated = sorted([o for o in bpy.data.objects if o.animation_data],
                      key=lambda o: o.name)
    print(f"animated_objects: {len(animated)}")
    for obj in animated:
        ad = obj.animation_data
        active = ad.action.name if ad.action else "-"
        print(f"object: {obj.name}")
        print(f"  active_action: {active}")
        print(f"  use_nla: {ad.use_nla}")
        print(f"  nla_tracks: {len(ad.nla_tracks)}")
        for track in ad.nla_tracks:
            print(f"  track: {track.name}: mute={track.mute}")
            for strip in sorted(track.strips, key=lambda s: (s.frame_start, s.name)):
                sact = strip.action.name if strip.action else "-"
                print(f"    strip: {strip.name}: action={sact} frames={fmt(strip.frame_start)}-{fmt(strip.frame_end)} repeat={fmt(strip.repeat)} scale={fmt(strip.scale)} blend={strip.blend_type} extrapolation={strip.extrapolation} mute={strip.mute}")


def main():
    args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
    if "--make-fixture" in args:
        make_fixture()
    audit()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
```
