en un clic
ae-mixin-developer
// Add methods to existing mixins or create new mixins for the After Effects automation client
// Add methods to existing mixins or create new mixins for the After Effects automation client
Manage After Effects from Claude — create configs, build compositions, render videos, diagnose issues, and optionally generate AI content via Prompture
Create and validate JSON automation configs for the After Effects automation pipeline
Write unit tests following the project's existing patterns and conventions
Create ExtendScript/JSX files that integrate with the Python-to-AE file-based command bridge
| name | ae-mixin-developer |
| description | Add methods to existing mixins or create new mixins for the After Effects automation client |
This skill covers adding methods to the mixin-based Client architecture and creating new mixins. The Client class in ae_automation/__init__.py composes six mixins via multiple inheritance.
The Client is assembled in ae_automation/__init__.py:
class Client(
afterEffectMixin, # ae_automation/mixins/afterEffect.py
ToolsMixin, # ae_automation/mixins/tools.py
botMixin, # ae_automation/mixins/bot.py
VideoEditorAppMixin, # ae_automation/mixins/VideoEditorApp.py
TemplateGeneratorMixin, # ae_automation/mixins/templateGenerator.py
ProcessManagerMixin, # ae_automation/mixins/processManager.py
):
JS_FRAMEWORK = ""
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Loads settings, creates cache folder, reads json2.js + framework.js
# Replaces {CACHE_FOLDER} in framework.js with actual path (forward slashes)
Methods from any mixin are accessible as self.methodName() on the Client instance. All mixins share self state.
The core bridge between Python and AE. Located in afterEffect.py:
def runScript(self, fileName, _remplacements=None, debug=False):
Keys must include braces. Values must be strings (it's string replacement, not formatting):
self.runScript("my_script.jsx", {
"{comp_name}": str(comp_name),
"{layer_name}": str(layer_name),
"{startTime}": str(start_time),
"{duration}": str(duration),
"{value}": str(value),
})
Common mistake: Forgetting braces in keys.
# WRONG -- placeholder won't be found
{"comp_name": str(comp_name)}
# CORRECT
{"{comp_name}": str(comp_name)}
settings.JS_DIRJS_FRAMEWORK (json2.js + framework.js)try { ... } catch(e) { ... } outputLogs(_error);settings.CACHE_FOLDERAlways call self.sanitize_text_for_ae() on any user-facing text before passing it to JSX. This method:
<br> tags (all variants) to AE carriage returns (\r)def myMethod(self, comp_name, layer_name, text):
sanitized = self.sanitize_text_for_ae(text)
self.runScript("my_script.jsx", {
"{comp_name}": str(comp_name),
"{layer_name}": str(layer_name),
"{value}": str(sanitized),
})
When to call it: Any time the value comes from user input or config and might contain HTML or special characters. The existing editComp() and editLayerAtKey() methods call it automatically.
JavaScript/ExtendScript requires forward slashes in file paths. When constructing paths for JSX:
# Convert backslashes to forward slashes for JS
js_path = path.replace("\\", "/")
self.runScript("import_file.jsx", {
"{filePath}": js_path,
})
The settings module handles this for CACHE_FOLDER already (see __init__.py constructor), but any new paths you construct must be converted manually.
self.hexToRGBA() converts hex color strings to AE's normalized RGBA format:
color = self.hexToRGBA("#FF0000")
# Returns: "1.0,0.0,0.0,1"
The returned string is comma-separated with values in 0.0-1.0 range, with alpha always 1. This is the format AE expects for color properties.
In parseCustomActions(), this conversion happens automatically when property_type == "color":
if "property_type" in custom_edit:
if custom_edit["property_type"] == "color":
custom_edit["value"] = self.hexToRGBA(custom_edit["value"])
The codebase uses mixed conventions. Follow the convention of the mixin you're modifying:
| Mixin | Convention | Examples |
|---|---|---|
afterEffectMixin | camelCase | startAfterEffect(), createComp(), editComp(), runScript() |
ToolsMixin | camelCase | hexToRGBA(), testFunction(), previewLogs() |
botMixin | camelCase | startBot() |
ProcessManagerMixin | snake_case | wait_for_process(), is_after_effects_responsive() |
TemplateGeneratorMixin | camelCase | createNewProject(), saveProject(), addTextLayer() |
VideoEditorAppMixin | camelCase | Flask routes |
When creating a new mixin, prefer snake_case (the modern convention used in ProcessManagerMixin).
After calling runScript(), you need to wait for AE to execute the script. The wait time depends on operation complexity:
# Simple property change
self.runScript("update_properties.jsx", replacements)
time.sleep(1)
# File import or composition creation
self.runScript("add_resource.jsx", replacements)
time.sleep(2)
# Complex operation (duplication, rendering setup)
self.runScript("duplicate_comp_2.jsx", replacements)
time.sleep(3)
# Very complex (full project creation)
self.runScript("create_new_project.jsx", replacements)
time.sleep(5)
The base runScript() already has a time.sleep(1) at the end. Additional sleeps in your method are on top of that.
For more reliable waiting, use the ProcessManagerMixin pattern:
self.safe_script_execution("my_script.jsx", replacements, wait_time=3)
JSX scripts can write data files that Python reads:
.jsx file):var result = { items: [] };
// ... populate result ...
saveFile("my_output.json", JSON.stringify(result));
import json
from ae_automation import settings
def getMyData(self, comp_name):
self.runScript("my_data_script.jsx", {
"{comp_name}": str(comp_name),
})
time.sleep(2) # wait for AE to write the file
output_path = os.path.join(settings.CACHE_FOLDER, "my_output.json")
with open(output_path, "r", encoding="utf-8") as f:
return json.load(f)
ae_automation/mixins/my_feature.py:
"""
My Feature Mixin
Provides functionality for [description]
"""
import os
import time
from ae_automation import settings
class MyFeatureMixin:
"""Mixin that adds [feature] capabilities to the Client."""
def do_something(self, comp_name, value):
"""
Description of what this method does.
Args:
comp_name: Name of the target composition
value: The value to set
"""
sanitized_value = self.sanitize_text_for_ae(value)
self.runScript("my_script.jsx", {
"{comp_name}": str(comp_name),
"{value}": str(sanitized_value),
})
time.sleep(2)
def get_data(self):
"""Retrieve data from AE."""
self.runScript("get_data.jsx")
time.sleep(2)
output_path = os.path.join(settings.CACHE_FOLDER, "data_output.json")
import json
with open(output_path, "r", encoding="utf-8") as f:
return json.load(f)
__init__.pyAdd the import and include it in the Client class:
from ae_automation.mixins.my_feature import MyFeatureMixin
class Client(
afterEffectMixin,
ToolsMixin,
botMixin,
VideoEditorAppMixin,
TemplateGeneratorMixin,
ProcessManagerMixin,
MyFeatureMixin, # Add here
):
Place them in ae_automation/mixins/js/ following the patterns in the jsx-script-creator skill.
Available via from ae_automation import settings:
| Constant | Description |
|---|---|
settings.CACHE_FOLDER | Cache directory for temp files and script output |
settings.QUEUE_FOLDER | Queue directory monitored by AE command runner |
settings.AFTER_EFFECT_FOLDER | AE installation path |
settings.AFTER_EFFECT_PROJECT_FOLDER | AE project folder name |
settings.AERENDER_PATH | Path to aerender.exe |
settings.PACKAGE_DIR | Root directory of the ae_automation package |
settings.JS_DIR | Directory containing JSX/JS scripts |
To add a new action type to the config system:
parseCustomActions()In ae_automation/mixins/afterEffect.py, add a new if block:
def parseCustomActions(self, custom_edit, scene_folder, itemTimeline, data):
# ... existing handlers ...
if custom_edit["change_type"] == "my_new_action":
self.myNewAction(
self.slug(scene_folder) + "_" + custom_edit["comp_name"],
custom_edit["other_field"],
)
Note the self.slug() prefix pattern: Scene-specific compositions are named {scene_slug}_{template_comp}. The slug() method (from ToolsMixin) creates a URL-safe version of the scene folder name.
def myNewAction(self, comp_name, other_field):
self.runScript("my_new_action.jsx", {
"{comp_name}": str(comp_name),
"{other_field}": str(other_field),
})
time.sleep(2)
ae_automation/mixins/js/my_new_action.jsx -- see the jsx-script-creator skill.
Add the new change_type to the config-schema reference.
ae_automation/mixins/js/toggle_layer_visibility.jsx)://
// Toggle Layer Visibility
// ------------------------------------------------------------
// Language: javascript
//
function toggleLayerVisibility(compName, layerName, visible) {
var comp = FindItemByName(compName);
if (!comp) {
print("Composition not found: " + compName);
return;
}
var layer = FindLayerByComp(compName, layerName);
if (!layer) {
print("Layer not found: " + layerName);
return;
}
layer.enabled = visible;
print("Set " + layerName + " visibility to " + visible);
}
toggleLayerVisibility("{comp_name}", "{layer_name}", {visible});
afterEffect.py or a new mixin):def toggleLayerVisibility(self, comp_name, layer_name, visible):
self.runScript("toggle_layer_visibility.jsx", {
"{comp_name}": str(comp_name),
"{layer_name}": str(layer_name),
"{visible}": "true" if visible else "false",
})
time.sleep(1)
parseCustomActions()):if custom_edit["change_type"] == "toggle_visibility":
self.toggleLayerVisibility(
self.slug(scene_folder) + "_" + custom_edit["comp_name"],
custom_edit["layer_name"],
custom_edit.get("visible", True),
)
{
"change_type": "toggle_visibility",
"comp_name": "IntroTemplate",
"layer_name": "Watermark",
"visible": false
}