| name | ae-mixin-developer |
| description | Add methods to existing mixins or create new mixins for the After Effects automation client |
AE Mixin Developer
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.
Mixin Composition
The Client is assembled in ae_automation/__init__.py:
class Client(
afterEffectMixin,
ToolsMixin,
botMixin,
VideoEditorAppMixin,
TemplateGeneratorMixin,
ProcessManagerMixin,
):
JS_FRAMEWORK = ""
def __init__(self, **kwargs):
super().__init__(**kwargs)
Methods from any mixin are accessible as self.methodName() on the Client instance. All mixins share self state.
The runScript() Pattern
The core bridge between Python and AE. Located in afterEffect.py:
def runScript(self, fileName, _remplacements=None, debug=False):
Replacement Dict Rules
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.
{"comp_name": str(comp_name)}
{"{comp_name}": str(comp_name)}
What runScript() Does
- Reads the JSX file from
settings.JS_DIR
- Applies all replacements from the dict
- Prepends minified
JS_FRAMEWORK (json2.js + framework.js)
- Wraps in
try { ... } catch(e) { ... } outputLogs(_error);
- Writes composed script to
settings.CACHE_FOLDER
- Sends to AE via the file-based queue system
- Returns a UUID string (useful for reading log output)
Text Sanitization
Always call self.sanitize_text_for_ae() on any user-facing text before passing it to JSX. This method:
- Converts HTML
<br> tags (all variants) to AE carriage returns (\r)
- Preserves straight quotes to avoid UTF-8 encoding issues
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.
Path Handling
JavaScript/ExtendScript requires forward slashes in file paths. When constructing paths for JSX:
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.
Color Handling
self.hexToRGBA() converts hex color strings to AE's normalized RGBA format:
color = self.hexToRGBA("#FF0000")
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"])
Method Naming Conventions
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).
Wait Patterns
After calling runScript(), you need to wait for AE to execute the script. The wait time depends on operation complexity:
self.runScript("update_properties.jsx", replacements)
time.sleep(1)
self.runScript("add_resource.jsx", replacements)
time.sleep(2)
self.runScript("duplicate_comp_2.jsx", replacements)
time.sleep(3)
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)
Reading Data Back from AE
JSX scripts can write data files that Python reads:
JSX side (in your .jsx file):
var result = { items: [] };
saveFile("my_output.json", JSON.stringify(result));
Python side (in your mixin method):
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)
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)
Creating a New Mixin
1. Create the mixin file
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)
2. Register the mixin in __init__.py
Add 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,
):
3. Create corresponding JSX scripts
Place them in ae_automation/mixins/js/ following the patterns in the jsx-script-creator skill.
Settings Module Constants
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 |
Adding a New change_type
To add a new action type to the config system:
1. Add the handler in parseCustomActions()
In ae_automation/mixins/afterEffect.py, add a new if block:
def parseCustomActions(self, custom_edit, scene_folder, itemTimeline, data):
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.
2. Implement the method
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)
3. Create the JSX script
ae_automation/mixins/js/my_new_action.jsx -- see the jsx-script-creator skill.
4. Document in config schema
Add the new change_type to the config-schema reference.
Complete Example: Adding a Layer Visibility Toggle
JSX script (ae_automation/mixins/js/toggle_layer_visibility.jsx):
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});
Python method (in 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)
Config integration (in 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),
)
Config usage:
{
"change_type": "toggle_visibility",
"comp_name": "IntroTemplate",
"layer_name": "Watermark",
"visible": false
}