agentleFS
blixvipblixvip/skills/ae-extendscript/SKILL.md

ae-extendscript

Skill4 starsChanged 3 months agoMITView on GitHub

Drive Adobe After Effects from an agent by writing and running ExtendScript (.jsx) — the result-file bridge that gives you real error feedback, the PNG frame-render loop that lets you see what you built, and the ES3 language traps that break AE scripts. Use whenever the task is to build, fix, keyframe, expression, render, or inspect anything inside After Effects, including "fix this comp", "the expression is broken", "keyframe these layers", "build this animation in AE", or "control After Effects with code".

The file

Show its rules and text

We split this skill into the rules it gives an agent

13 rules. About 0 are common, 8 are distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈8

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    After Effects has no chat interface and no MCP by default.

    line 8
  2. Distinctive:

    Do not click. Do not describe steps for the user to perform.

    line 10
  3. Distinctive:

    Every script you generate is wrapped like this.

    line 48
  4. Very specific:

    Paths: use forward slashes in new File("C:/...").

    line 115
  5. Very specific:

    $.writeln goes nowhere useful: when run headless — log to the result file instead.

    line 117
  6. Distinctive:

    After Effects is the right engine when the graphic must live on the footage: tracked elements, plate integration, roto, real-camera 3D, …

    line 136
  7. Distinctive:

    When the user says a comp is broken:

    line 142
  8. Distinctive:

    Report what you found: before you fix it. The user often knows which of the four things they meant.

    line 145
  9. Distinctive:

    Put related layers in a pre-comp or a labelled group.

    line 156
  10. Very specific:

    Do not app.project.save() over the user's file without being asked.

    line 157
  11. Very specific:

    All snippets assume they sit inside the wrapper from SKILL.md (undo group + try/catch + result file).

    line 166
  12. Distinctive:

    Prefer expressions over hundreds of keyframes for follow, wiggle, loop, and rig behaviour — they survive a timing change.

    line 329
  13. Distinctive:

    Run this first on any "my project is broken" request.

    line 407

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

ae-extendscript

After Effects has no chat interface and no MCP by default. It has a scripting engine. You control it by writing a .jsx file, running it, and reading a result file back — a real feedback loop, not fire-and-forget.

Do not click. Do not describe steps for the user to perform. Write the script.

One-time setup (tell the user, once)

Enable file writing — without it every result file silently fails:

Edit → Preferences → Scripting & Expressions → tick Allow Scripts to Write Files and Access Network → OK

That is the only manual step. If a script "runs but nothing happened", this is the first thing to check.

The bridge

Three files, every time:

work/
  ae/run.jsx        the script you generate
  ae/result.json    what AE writes back — you read this

Run it

Windows (adjust the year to the installed version):

"/c/Program Files/Adobe/Adobe After Effects 2025/Support Files/AfterFX.exe" -r "C:/work/ae/run.jsx"

macOS:

osascript -e 'tell application "Adobe After Effects 2025" to DoScript "$.evalFile(\"/work/ae/run.jsx\")"'

-r runs the script in the already-open instance and leaves AE open. It returns immediately — it does not wait, and it does not report errors. That is exactly why the result file exists.

The wrapper — never write a bare script

Every script you generate is wrapped like this. The wrapper is what converts a silent failure into a message you can act on.

(function () {
    var LOG = [];
    var OUT = new File("C:/work/ae/result.json");
    function log(m) { LOG.push(String(m)); }
    function write(status, err) {
        OUT.encoding = "UTF-8";
        OUT.open("w");
        OUT.write('{"status":"' + status + '","error":' +
                  (err ? '"' + String(err).replace(/"/g, "'") + '"' : 'null') +
                  ',"log":["' + LOG.join('","') + '"]}');
        OUT.close();
    }

    app.beginUndoGroup("agent-run");
    try {

        // ---- work goes here ----
        log("started");

        write("ok", null);
    } catch (e) {
        write("error", e.toString() + " @line " + e.line);
    } finally {
        app.endUndoGroup();
    }
})();

Then read result.json. status: "error" gives you the message and the line — fix and re-run. Never assume it worked.

app.beginUndoGroup / endUndoGroup is mandatory. It makes the entire agent run a single Ctrl+Z for the user. Without it you have scattered their undo history and they cannot back out of your work.

Seeing what you built

You are not blind. CompItem.saveFrameToPng renders a single frame to disk:

comp.saveFrameToPng(comp.time, new File("C:/work/ae/preview.png"));
// or a specific time, in seconds:
comp.saveFrameToPng(1.5, new File("C:/work/ae/f_1500.png"));

Render 3–5 frames across the animation, then read them as images. This closes the loop: build → render frames → look → correct. Do this before telling the user anything is done. A composition that scripts cleanly can still be visually wrong.

The ES3 traps that break AE scripts

The ExtendScript engine is ancient JavaScript. Modern syntax does not fail loudly — it fails at parse time, which means the whole file does nothing.

Do not useUse instead
let / constvar
Arrow functionsfunction () {}
Template literalsstring concatenation with +
JSON.parse / JSON.stringifybuild strings by hand (as in the wrapper above)
Array.forEach / map / filterfor (var i = 0; i < a.length; i++)
String.trim, Array.indexOfwrite the helper yourself
Default / rest parametersarguments, and explicit defaults
classfunction constructors

Other traps that cost real time:

  • Time is in seconds, not frames. Convert with comp.frameDuration (frame * comp.frameDuration).
  • Collections are 1-indexed. comp.layer(1) is the top layer; app.project.item(1) is the first item.
  • Colors are arrays of 0–1 floats, not 0–255 and not hex. Write a hex() helper (see Appendix A).
  • Paths: use forward slashes in new File("C:/..."). Backslashes need doubling and will bite you.
  • Text layers: you cannot mutate the TextDocument in place. Read it into a var, change it, assign it back.
  • $.writeln goes nowhere useful when run headless — log to the result file instead.
  • app.project.file is null on an unsaved project. Guard before deriving paths from it.

Discover, do not guess

Effect and property identifiers are matchName strings like "ADBE Gaussian Blur 2". Guessing them wastes runs. Dump the real ones from a layer the user already set up:

var L = app.project.activeItem.layer(1);
var fx = L.property("ADBE Effect Parade");
for (var i = 1; i <= fx.numProperties; i++) {
    log(fx.property(i).name + "  ->  " + fx.property(i).matchName);
}

Same pattern for any property group. When you need an effect you have not used before, ask the user to apply it once by hand to a scratch layer, dump its matchName and parameter names, then script it forever after.

What to build in AE — and what not to

After Effects is the right engine when the graphic must live on the footage: tracked elements, plate integration, roto, real-camera 3D, particles, complex masks and mattes, and any fix to a project the user already has open.

It is the wrong engine for large programmatic sequences, data-driven charts, or anything you would rather express as code — those belong in Remotion or HyperFrames, rendered with alpha and imported. See motion-pipeline for the engine table.

Fixing an existing project

When the user says a comp is broken:

  1. Inspect first, change nothing. Script a walk of comps → layers → properties, logging expression errors, missing footage, and disabled layers. Read the result.
  2. Report what you found before you fix it. The user often knows which of the four things they meant.
  3. Fix inside one undo group.
  4. Render frames and confirm.

The most common real breakages: an expression referencing a renamed layer, an effect whose parameter index shifted, footage relinked to a different frame rate, and a parent chain that lost its null.

Rules

  • Keyframes are worthless without easing. Every setValueAtTime is followed by setTemporalEaseAtKey — see Appendix A. Default linear keyframes are the AE equivalent of a flat background.
  • Set layer.startTime / stagger entrances; never bring elements on together.
  • Name every layer you create. Shape Layer 7 is unmaintainable for the human who inherits the comp.
  • Put related layers in a pre-comp or a labelled group. The user has to work in this project after you leave.
  • Do not app.project.save() over the user's file without being asked. Use saveWithDialog, or save a copy alongside.

Appendix A has working code for comps, text, shapes, eased keyframes, expressions, 3D cameras, effects, imports, frame renders, and the render queue.


Appendix A · ExtendScript recipes

All snippets assume they sit inside the wrapper from SKILL.md (undo group + try/catch + result file). ES3 only — var, no arrows, no template literals.

Helpers — paste these at the top

function hex(h) {                     // "#e8721f" -> [r,g,b] in 0..1
    h = h.replace("#", "");
    return [parseInt(h.substr(0,2),16)/255,
            parseInt(h.substr(2,2),16)/255,
            parseInt(h.substr(4,2),16)/255];
}
function f(comp, n) { return n * comp.frameDuration; }   // frames -> seconds

// Eased keyframes. inSpeed/outSpeed 0, influence 0..100.
// Big outgoing influence on the first key = "out-expo" feel.
function ease(prop, keyIndex, inInf, outInf) {
    var eIn  = new KeyframeEase(0, inInf  || 33);
    var eOut = new KeyframeEase(0, outInf || 33);
    var dim  = prop.propertyValueType === PropertyValueType.ThreeD_SPATIAL ||
               prop.propertyValueType === PropertyValueType.ThreeD ? 3 :
               (prop.propertyValueType === PropertyValueType.TwoD_SPATIAL ||
                prop.propertyValueType === PropertyValueType.TwoD ? 2 : 1);
    var aIn = [], aOut = [];
    for (var i = 0; i < dim; i++) { aIn.push(eIn); aOut.push(eOut); }
    prop.setInterpolationTypeAtKey(keyIndex,
        KeyframeInterpolationType.BEZIER, KeyframeInterpolationType.BEZIER);
    prop.setTemporalEaseAtKey(keyIndex, aIn, aOut);
}

// Two eased keys in one call — the move you will use most.
function animate(prop, t0, v0, t1, v1, inInf, outInf) {
    prop.setValueAtTime(t0, v0);
    prop.setValueAtTime(t1, v1);
    ease(prop, 1, 0,  outInf === undefined ? 85 : outInf);   // launch fast
    ease(prop, 2, inInf === undefined ? 75 : inInf, 0);      // settle slow
}

animate(p, 0, 0, 0.6, 100) with the defaults gives a strong ease-out — the premium feel. Symmetric influence (33/33) is the AE default and reads as generic.

Composition

var comp = app.project.items.addComp("SC01_hook", 1920, 1080, 1, 6, 30);
comp.bgColor = hex("#12100e");
comp.openInViewer();

Existing comp by name:

function findComp(name) {
    for (var i = 1; i <= app.project.numItems; i++) {
        var it = app.project.item(i);
        if (it instanceof CompItem && it.name === name) return it;
    }
    return null;
}
var comp = findComp("SC01_hook") || app.project.activeItem;

Text layer

var t = comp.layers.addText("650M views");
t.name = "stat";

var src = t.property("Source Text");
var td  = src.value;              // read out
td.font       = "Inter-Bold";     // PostScript name, not the display name
td.fontSize   = 140;
td.fillColor  = hex("#faf8f5");
td.tracking   = -30;
td.justification = ParagraphJustification.LEFT_JUSTIFY;
src.setValue(td);                 // assign back — mutating td alone does nothing

t.property("Transform").property("Position").setValue([160, 620]);

Find PostScript font names by dumping them once: for (var i=0;i<app.fonts.allFonts.length;i++) log(app.fonts.allFonts[i].postScriptName);

Shape, solid, null, camera, light

var sh   = comp.layers.addShape();
var sol  = comp.layers.addSolid(hex("#1c1917"), "bg", comp.width, comp.height, 1, comp.duration);
var nul  = comp.layers.addNull(comp.duration);
var cam  = comp.layers.addCamera("cam", [comp.width/2, comp.height/2]);
var lit  = comp.layers.addLight("key", [comp.width/2, comp.height/2]);
lit.lightType = LightType.POINT;
lit.property("Intensity").setValue(120);
lit.property("Color").setValue(hex("#e8721f"));

The signature entrance: pop-blur-fade, staggered

function popIn(layer, t0) {
    var tr = layer.property("Transform");
    animate(tr.property("Opacity"),  t0, 0, t0 + 0.45, 100);
    animate(tr.property("Scale"),    t0, [92,92], t0 + 0.55, [100,100]);

    var p = tr.property("Position").value;
    animate(tr.property("Position"), t0, [p[0], p[1] + 40], t0 + 0.55, p);

    var fx = layer.property("ADBE Effect Parade").addProperty("ADBE Gaussian Blur 2");
    var b  = fx.property("ADBE Gaussian Blur 2-0001");   // Blurriness
    animate(b, t0, 24, t0 + 0.40, 0);
    fx.property("ADBE Gaussian Blur 2-0003").setValue(true); // repeat edge pixels
}

// stagger — never bring them on together
var layers = [l1, l2, l3];
for (var i = 0; i < layers.length; i++) popIn(layers[i], 0.2 + i * 0.09);

Parenting and a 3D camera push

child.parent = nul;               // null is now the rig

cam.threeDLayer = true;
var pos = cam.property("Transform").property("Position");
animate(pos, 0, [960, 540, -2600], comp.duration, [960, 540, -2050], 40, 40);

Slow, continuous, gently eased. A camera that stops moving makes the frame read as a slide.

Effects, by matchName

var parade = layer.property("ADBE Effect Parade");
var glow   = parade.addProperty("ADBE Glo2");     // Glow
var fill   = parade.addProperty("ADBE Fill");
fill.property("Color").setValue(hex("#e8721f"));

Do not guess matchNames. Apply the effect once by hand, then dump it:

var fx = layer.property("ADBE Effect Parade");
for (var i = 1; i <= fx.numProperties; i++) {
    var e = fx.property(i);
    log(e.name + " -> " + e.matchName);
    for (var j = 1; j <= e.numProperties; j++)
        log("    " + e.property(j).name + " -> " + e.property(j).matchName);
}

Expressions

var p = layer.property("Transform").property("Position");
p.expression =
    "var lead = thisComp.layer('ball');\n" +
    "var q = lead.transform.position;\n" +
    "[q[0], 980];";

Rules that prevent the usual breakage:

  • Reference layers by name, and make sure the name is one you set — renamed layers are the top cause of expression errors.
  • Wrap anything that can fail: try { ... } catch (e) { value } so a missing layer degrades instead of erroring.
  • Prefer expressions over hundreds of keyframes for follow, wiggle, loop, and rig behaviour — they survive a timing change; keyframes do not.
  • To find existing errors, walk every property and read prop.expressionError.

Useful patterns:

// inertial follow
delay = 0.08; thisComp.layer("lead").transform.position.valueAtTime(time - delay)

// loop the last two keys
loopOut("cycle")

// eased, expression-driven entrance (no keyframes at all)
t = time - inPoint; d = 0.6;
ease(t, 0, d, 40, 0)

Import footage and place it

var io = new ImportOptions(new File("C:/work/06-renders/sc03.mov"));
if (io.canImportAs(ImportAsType.FOOTAGE)) io.importAs = ImportAsType.FOOTAGE;
var item  = app.project.importFile(io);
var layer = comp.layers.add(item);
layer.startTime = 2.4;

A ProRes 4444 render from Remotion or HyperFrames imports with alpha intact and drops straight onto the plate — the normal way to combine engines.

Render a frame (your eyes)

comp.saveFrameToPng(0.0,  new File("C:/work/ae/f000.png"));
comp.saveFrameToPng(1.0,  new File("C:/work/ae/f100.png"));
comp.saveFrameToPng(comp.duration - comp.frameDuration,
                    new File("C:/work/ae/fend.png"));

Then read the PNGs. Do this before reporting completion.

Render queue

var rq = app.project.renderQueue;
var it = rq.items.add(comp);
it.outputModule(1).applyTemplate("Lossless with Alpha");
it.outputModule(1).file = new File("C:/work/06-renders/" + comp.name + ".mov");
rq.render();

rq.render() blocks until done — unlike -r, so the run genuinely finishes before your result file is written. Template names must already exist in the user's AE; list them with it.outputModule(1).templates.

Inspect a broken project

for (var i = 1; i <= app.project.numItems; i++) {
    var it = app.project.item(i);
    if (it instanceof FootageItem && it.footageMissing) log("MISSING: " + it.name);
    if (!(it instanceof CompItem)) continue;
    log("COMP " + it.name + " " + it.width + "x" + it.height + " @" + it.frameRate);
    for (var L = 1; L <= it.numLayers; L++) {
        var lay = it.layer(L);
        if (!lay.enabled) log("  disabled: " + lay.name);
        scanProps(lay, "  " + lay.name);
    }
}
function scanProps(g, path) {
    for (var i = 1; i <= g.numProperties; i++) {
        var p = g.property(i);
        if (p.numProperties !== undefined && p.numProperties > 0) {
            scanProps(p, path + "/" + p.name);
        } else if (p.canSetExpression && p.expressionEnabled && p.expressionError) {
            log("EXPR ERROR " + path + "/" + p.name + ": " + p.expressionError);
        }
    }
}

Run this first on any "my project is broken" request. Report before repairing.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.