Write your first script
- Open Code from the Window menu.
- Choose New code, then Script. A new script opens with a starter:
import grapple as gp def run(ctx): print(ctx.project.composition().name) - Press Run, or Ctrl+Enter. The Output tab shows the name of the composition you have open, and the footer says how long the run took and how many changes it made.
A script is a module that defines run(ctx). Grapple runs the module, then calls run once. While it runs, the button reads Stop; press it, or Esc, to stop the script.
What a script receives
ctx.project- The open project.
ctx.project.composition()is the composition that was open when you pressed Run, andctx.project.composition("Intro")finds one by name. ctx.selection- The clips that were selected when you pressed Run.
Every object you reach through them is a handle: a composition, layer, clip, camera, property or asset. A handle's attributes read the project, such as clip.start, and its methods change it, such as clip.move(2.0). To list everything a handle offers while you work, print gp.api("Clip"), or gp.api() for all of it.
How changes land
- Each change appears in the Changes tab as it happens, as a link to the object it touched.
- When the script finishes, all of its changes apply together as one History row, "Ran Python script". One undo takes them all back.
- Later reads in the same run see earlier changes. Add a clip, and the next line can read it.
- If the script raises an error, or you stop it, nothing it did is kept. The footer says where it stopped, such as "Stopped at line 12 after 3 changes", and the Problems tab names the line and the error.
- A script that changes nothing adds no row to History.
Read the project
def run(ctx):
for comp in ctx.project.compositions():
print(comp.name, comp.width, comp.height, comp.fps, comp.duration)
comp = ctx.project.composition()
for layer in comp.layers():
print("Layer", layer.name)
for clip in comp.clips():
print(f"{clip.name}: {clip.kind}, {clip.start:.2f} s for {clip.duration:.2f} s")
Names like comp.name and clip.start are attributes, read without brackets. Lists such as comp.layers() and comp.clips() are methods. comp.find("Title") returns the layer, clip or camera with that name, or None. Times are in seconds of the composition.
Make things
- Compositions
ctx.project.new_composition("Square", width=1080, height=1080). The frame rate defaults to 30.- Layers
comp.add_layer("Titles")makes a visual layer on top, or above or below another withabove=orbelow=.- Text
comp.add_text("Hello", "Hello", start=1.0, duration=3.0). A text clip's name is its text, so the first two arguments are the same. Leave outlayer=and the clip gets a new layer on top.- Shapes
comp.add_shape("ellipse", "ellipse", start=0, duration=4). The kind isrectangle,rounded_rectangleorellipse, and it is also the clip's name.- Media
comp.add_media(asset, start=0, source_start=12.5, duration=4)places an imported file. Find the asset inctx.project.assets()by itsname. Leave outdurationto play the rest of the source.- Cameras
comp.add_camera("Orbit").- Remove
- Compositions, layers, clips, cameras, assets and operators each have
remove().
Set values and keys
A clip, layer or camera reads a property with get and sets one with set, by the name the Inspector shows or by its id. Values use the Inspector's units: pixels, degrees, and percent for scale and opacity.
title.set("Opacity", 80)
print(title.get("Opacity", time=2.0)) # the value at 2 seconds, after keys and drivers
Position, Rotation, Scale and Anchor are attributes holding a gp.Vec3. Set all three axes, or a dictionary of the ones you want to change:
p = title.position
title.position = gp.Vec3(p.x, p.y - 40, p.z) # 40 pixels higher
title.scale = {"x": 120, "y": 120} # z keeps its value
For keys, find the property among properties() and give set_keys a list of (seconds, value, interpolation). The interpolation says how the value leaves that key: hold, linear, ease_in, ease_out, ease_in_out, smooth_through or overshoot. set_keys replaces the property's keys.
opacity = next(p for p in title.properties() if p.name == "Opacity")
start = title.start
opacity.set_keys([(start, 0, "ease_out"), (start + 0.5, 100, "linear")])
Position, Scale and Anchor key all their axes together unless you separate them: prop.set_dimensions("separate") gives each axis its own keys, and set_keys(..., component_index=0) then keys X alone. Rotation always keys each axis on its own. prop.clear_animation() removes the keys.
Add a live expression
set_expression puts an expression on a property, the same as Add expression in the Inspector. Give it the source, and a dictionary of other properties the expression reads by name:
opacity = next(p for p in clip.properties() if p.name == "Opacity")
opacity.set_expression(
{"source": "value * (0.85 + 0.15 * gp.noise(3, 'flicker', time * 8))", "form": "expression"},
{})
Pass a plain string instead of the dictionary for a function that defines evaluate(ctx). The expressions guide covers both forms. For a controller that drives several properties from one function, use comp.add_operator; see controllers.
Worked examples
Save, open and export
A new script is called Untitled script until you name it. Save it, and it joins the Scripts group in Code, saved with the project. In the Code actions menu, Rename… changes its name, Open script… brings in a .py file, and Export script… writes the open script to one. A script can import your project's modules, as grapple_project. and the module's name.
Your scripts and Ape
Ape writes its own scripts against gp, a richer API with handles for every kind of object, typed times, rendering and measuring. The two are separate: a script you run from Code uses ctx.project as shown here, and Ape's scripts use gp. To have Ape do something with Python, ask it in the Ape tab. Scripts with gp shows how Ape's scripts read.
The script API
Everything a script run from Code can call, read from the list Grapple hands every run. Types: seconds are composition seconds unless a parameter says otherwise, and value is a number, a gp value or a dictionary of axes, in the property's units.
ctx
What run(ctx) receives.
ctx.project: ProjectattributeThe open project.
ctx.selection: list[Clip]attributeRead the clips selected when Run was pressed.
ctx.project
The open project.
ctx.project.compositions() → list[Composition]List the project's compositions.
ctx.project.composition(name: str | None = None) → CompositionFind a composition by name, or the composition open when Run was pressed.
ctx.project.assets() → list[Asset]List admitted project assets.
ctx.project.modules() → list[{name:str,kind:str}]List project Python sources with each library or script kind.
ctx.project.new_composition(name: str, width: int = 1920, height: int = 1080, fps: Fraction = 30) → CompositionCreate a composition with pixel dimensions and frames per second.
Units:
widthin pixels,heightin pixels,fpsin frames per second.ctx.project.apply(score: Score) → NoneApply the compositions, layers, media clips, text, shapes, visual assemblies, and collections in this score.
ctx.project.read_module(name: str) → strRead one project Python module's source.
ctx.project.write_module(name: str, source: str, kind: str = 'library', previous_name: str | None = None) → NoneWrite one project Python library or script, or rename one with previous_name. Libraries import as grapple_project.<name>; scripts run by name.
ctx.project.delete_module(name: str) → NoneDelete one project Python module.
Composition
A composition, from ctx.project.composition() or ctx.project.compositions().
composition.name: strattributeRead the composition name.
composition.width: intattributeRead the composition's pixel width.
composition.height: intattributeRead the composition's pixel height.
composition.fps: FractionattributeRead the composition's frames per second.
composition.duration: floatattributeRead the composition duration in composition seconds.
composition.layers() → list[Layer]List every composition layer top to bottom in Timeline order, including hidden and disabled layers; audio layers follow visual layers.
composition.clips() → list[Clip]List the composition's clips in timeline order.
composition.cameras() → list[Camera]List the composition's cameras.
composition.find(name: str) → Layer|Clip|Camera|NoneFind a named object in this composition.
composition.add_layer(name: str, above: Layer | None = None, below: Layer | None = None) → LayerCreate a visual layer on top, or above or below the given layer. Supply at most one of above and below.
composition.add_text(name: str, text: str, start: seconds = 0, duration: seconds = 10, layer: Layer | None = None) → ClipCreate an editable text clip at composition seconds; its native name is its text, so name and text must match. Omit layer to create a new layer at the top.
Units:
startin composition seconds,durationin seconds.composition.add_shape(name: str, kind: str = 'rectangle', start: seconds = 0, duration: seconds = 10, layer: Layer | None = None) → ClipCreate an editable shape clip at composition seconds; its native name is its kind, so name and kind must match. Omit layer to create a new layer at the top.
Units:
startin composition seconds,durationin seconds.composition.add_media(asset: Asset, start: seconds = 0, duration: seconds | None = None, layer: Layer | None = None, source_start: seconds = 0) → ClipPlace an admitted asset at composition start, playing from source_start seconds for duration seconds. Omit duration to use the remaining source duration. Visual media starts at 100% contain fit; still images require source_start=0. Omit layer to create a new layer at the top.
Units:
startin composition seconds,durationin seconds,source_startin source seconds.composition.add_camera(name: str) → CameraCreate a composition camera.
composition.add_operator(name: str, source: str, params: dict[str,{default:value,min:float,max:float,step:float,label:str,description:str}], outputs: dict[str,Property]) → OperatorCreate one live Python controller. Each param has a typed default and numeric min/max when needed; each output binds a port to an editable target property.
composition.add_collection(generator: dict[str,value], templates: list[(str,Clip)], params: dict[str,float] = {}) → CollectionMake the clips, which share one layer, the templates of one collection. generator is {'kind': ...} with the kind's fields: a layout ('horizontal', 'vertical', 'grid', 'radial', 'along_path', 'responsive_fit') places one member per template; 'repeat' makes 'count' copies of one template in an 'arrangement' of 'row', 'grid' or 'circle'. templates pairs each member key with its clip, in order; the first clip keeps its id and its animation, which then drives every member. params sets the generator's number Params by name.
composition.remove() → NoneDelete this composition and its contents; surviving users drop the link, and one Undo restores the deletion.
Layer
A layer of a composition.
layer.name: strattributeRead the layer name.
layer.remove() → NoneDelete this layer and every clip on it; surviving users drop the link, and one Undo restores the deletion.
layer.properties() → list[Property]List the layer's registered properties.
layer.get(property: str, time: seconds | None = None, component_index: int | None = None) → valueRead a layer property at composition time, including animation and live drivers.
Units:
timein composition seconds.layer.set(property: str, value: value, component_index: int | None = None) → NoneSet a registered layer property. Target references use a layer or clip id; an empty string uses the fixed target position.
Units:
valuein the property's displayed units.layer.id() → strRead the layer id for property references.
layer.position: gp.Vec3attribute, settableRead or set x, y and z as gp.Vec3. Position and anchor use composition pixels with z increasing toward the camera, rotation uses degrees about each axis, and scale uses percent. Setting a dictionary of some axes keeps the others.
layer.rotation: gp.Vec3attribute, settableRead or set x, y and z as gp.Vec3. Position and anchor use composition pixels with z increasing toward the camera, rotation uses degrees about each axis, and scale uses percent. Setting a dictionary of some axes keeps the others.
layer.scale: gp.Vec3attribute, settableRead or set x, y and z as gp.Vec3. Position and anchor use composition pixels with z increasing toward the camera, rotation uses degrees about each axis, and scale uses percent. Setting a dictionary of some axes keeps the others.
layer.anchor: gp.Vec3attribute, settableRead or set x, y and z as gp.Vec3. Position and anchor use composition pixels with z increasing toward the camera, rotation uses degrees about each axis, and scale uses percent. Setting a dictionary of some axes keeps the others.
Clip
A clip: media, text or a shape.
clip.id() → strRead the clip id for property references.
clip.name: strattributeRead the clip's display name.
clip.kind: strattributeRead the clip kind.
clip.start: floatattributeRead the start in composition seconds.
clip.duration: floatattributeRead the duration in composition seconds.
clip.layer: LayerattributeRead the containing layer.
clip.text_runs() → list[TextStyleRun]Read the text clip's fixed half-open Unicode code point style ranges in pixels. An empty list means the whole text uses the clip style.
clip.properties() → list[Property]List the clip's available properties.
clip.get(property: str, time: seconds | None = None, component_index: int | None = None) → valueRead a clip property's evaluated value at composition time in its displayed units, including animation and live drivers.
Units:
timein composition seconds.clip.set(property: str, value: value, component_index: int | None = None) → NoneSet a clip property in its displayed units.
Units:
valuein the property's displayed units.clip.rename(name: str) → NoneChange a text clip's displayed name, which is its editable text; other clip kinds have no separate native name.
clip.move(start: seconds) → NoneMove the clip to a composition time in seconds.
Units:
startin composition seconds.clip.trim(start: seconds, duration: seconds) → NoneReplace the clip's composition time range in seconds.
Units:
startin composition seconds,durationin seconds.clip.remove() → NoneDelete this editable clip and its owned project objects; surviving users drop the link, and one Undo restores the deletion.
clip.set_text(text: str, style: TextStyle = None, runs: list[TextStyleRun] = None) → NoneReplace a text clip's content. style changes the clip style; runs replaces its fixed half-open Unicode code point ranges, and an empty list clears them. Sizes, tracking, baseline shifts, and outline widths are pixels; gradient points use text box pixels. Clear fill_paint with None before editing color; do not supply a color and a paint together.
Units:
stylein pixel lengths and text box pixel coordinates,runsin Unicode code point ranges; style lengths and coordinates in pixels.clip.position: gp.Vec3attribute, settableRead or set x, y and z as gp.Vec3. Position and anchor use composition pixels with z increasing toward the camera, rotation uses degrees about each axis, and scale uses percent. Setting a dictionary of some axes keeps the others.
clip.rotation: gp.Vec3attribute, settableRead or set x, y and z as gp.Vec3. Position and anchor use composition pixels with z increasing toward the camera, rotation uses degrees about each axis, and scale uses percent. Setting a dictionary of some axes keeps the others.
clip.scale: gp.Vec3attribute, settableRead or set x, y and z as gp.Vec3. Position and anchor use composition pixels with z increasing toward the camera, rotation uses degrees about each axis, and scale uses percent. Setting a dictionary of some axes keeps the others.
clip.anchor: gp.Vec3attribute, settableRead or set x, y and z as gp.Vec3. Position and anchor use composition pixels with z increasing toward the camera, rotation uses degrees about each axis, and scale uses percent. Setting a dictionary of some axes keeps the others.
Camera
A composition camera.
camera.properties() → list[Property]List the camera's registered properties.
camera.get(property: str, time: seconds | None = None, component_index: int | None = None) → valueRead a camera property at composition time, including animation and live drivers.
Units:
timein composition seconds.camera.set(property: str, value: value, component_index: int | None = None) → NoneSet a registered camera property. Target references use a layer or clip id; an empty string uses the fixed target position.
Units:
valuein the property's displayed units.camera.name: strattributeRead the camera name.
camera.remove() → NoneDelete this camera; a composition that viewed through it uses its next camera, surviving users drop the link, and one Undo restores the deletion.
camera.position: gp.Vec3attribute, settableRead or set x, y and z as gp.Vec3. Position and anchor use composition pixels with z increasing toward the camera, rotation uses degrees about each axis, and scale uses percent. Setting a dictionary of some axes keeps the others.
camera.rotation: gp.Vec3attribute, settableRead or set x, y and z as gp.Vec3. Position and anchor use composition pixels with z increasing toward the camera, rotation uses degrees about each axis, and scale uses percent. Setting a dictionary of some axes keeps the others.
camera.anchor: gp.Vec3attribute, settableRead or set x, y and z as gp.Vec3. Position and anchor use composition pixels with z increasing toward the camera, rotation uses degrees about each axis, and scale uses percent. Setting a dictionary of some axes keeps the others.
Property
One property of a clip, layer or camera, from properties().
property.id() → strRead this property's stable id.
property.name: strattributeRead this property's display name.
property.type: strattributeRead this property's value type.
property.animatable: boolattributeRead whether this property accepts keyframes or a live driver.
property.value_at(time: seconds, component_index: int | None = None) → valueRead this property's evaluated value at composition time in its displayed units, including animation and live drivers.
Units:
timein composition seconds.property.set_keys(keys: list[(seconds,value,interpolation)], tangents: list[{incoming:(seconds,value),outgoing:(seconds,value)}] = None, component_index: int | None = None) → NoneReplace all keyframes with (seconds, value, interpolation) triples. Interpolation is hold, linear, ease_in, ease_out, ease_in_out, smooth_through or overshoot. Tangents is an optional aligned list of incoming/outgoing (seconds offset, value offset) pairs. Positions and sizes use pixels, rotation uses degrees, and scale and opacity use percent. Transform values are gp.Vec3 or dictionaries of x, y and z; omitted axes keep their values. Position, Scale and Anchor animate as a whole by default: each key holds x, y and z, so axes keyed together share key times. Rotation, and a transform whose dimensions are separate, keys each axis on its own; set_dimensions separates or joins them, and component_index selects an axis. Paths interpolate when fill rules, contours, closed states and point ids match; script-built paths identify points by contour and point order.
Units:
keysin composition seconds and the property's displayed value units,tangentsin seconds and displayed value offsets.property.set_expression(program: PythonProgram, inputs: dict[str,Property], component_index: int | None = None) → NoneSet a live Python driver for an animatable property; component_index drives one axis of a transform. The output and property inputs use composition pixels for positions and sizes, and percent for scale and opacity.
property.set_dimensions(dimensions: str, resample: bool = False) → NoneChoose how Position, Scale or Anchor animates: 'whole', where each key holds x, y and z, or 'separate', where each axis has its own keys and timing. Rotation always keys each axis on its own. Separating keeps every key exactly. Joining axes keyed at different times needs resample=True.
property.clear_animation(component_index: int | None = None) → NoneClear this property's animation; a property without animation is unchanged.
Asset
An imported media file.
asset.name: strattributeRead the admitted media asset's name.
asset.rename(name: str) → NoneRename this asset while keeping its media and every use.
asset.remove() → NoneDelete this asset and its owned project objects; surviving users drop the link, storyboard shots keep their notes and lose the image, and stored media is retained for Undo.
Collection
A collection made by add_collection.
collection.members(time: seconds = 0) → list[Member]List the members at composition time: each gp.Member has its key, its template's key, and its layout values by property id.
Units:
timein composition seconds.
Operator
A live Python controller made by add_operator.
operator.remove() → NoneDelete this live controller and its owned output bindings; surviving users drop the link, and one Undo restores the deletion.