Your scripts in Code.

A script is Python you run once, when you press Run. Use one for the repetitive part of a job: lay out twenty lower thirds, stagger a selection, put the same fade on every title. Everything a script changes lands in History as one step, so one undo takes it all back.

Write your first script

  1. Open Code from the Window menu.
  2. Choose New code, then Script. A new script opens with a starter:
    import grapple as gp
    
    
    def run(ctx):
        print(ctx.project.composition().name)
  3. 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, and ctx.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 with above= or below=.
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 out layer= and the clip gets a new layer on top.
Shapes
comp.add_shape("ellipse", "ellipse", start=0, duration=4). The kind is rectangle, rounded_rectangle or ellipse, 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 in ctx.project.assets() by its name. Leave out duration to 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: Project attribute

The open project.

ctx.selection: list[Clip] attribute

Read 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) → Composition

Find 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) → Composition

Create a composition with pixel dimensions and frames per second.

Units: width in pixels, height in pixels, fps in frames per second.

ctx.project.apply(score: Score) → None

Apply the compositions, layers, media clips, text, shapes, visual assemblies, and collections in this score.

ctx.project.read_module(name: str) → str

Read one project Python module's source.

ctx.project.write_module(name: str, source: str, kind: str = 'library', previous_name: str | None = None) → None

Write 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) → None

Delete one project Python module.

Composition

A composition, from ctx.project.composition() or ctx.project.compositions().

composition.name: str attribute

Read the composition name.

composition.width: int attribute

Read the composition's pixel width.

composition.height: int attribute

Read the composition's pixel height.

composition.fps: Fraction attribute

Read the composition's frames per second.

composition.duration: float attribute

Read 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|None

Find a named object in this composition.

composition.add_layer(name: str, above: Layer | None = None, below: Layer | None = None) → Layer

Create 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) → Clip

Create 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: start in composition seconds, duration in seconds.

composition.add_shape(name: str, kind: str = 'rectangle', start: seconds = 0, duration: seconds = 10, layer: Layer | None = None) → Clip

Create 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: start in composition seconds, duration in seconds.

composition.add_media(asset: Asset, start: seconds = 0, duration: seconds | None = None, layer: Layer | None = None, source_start: seconds = 0) → Clip

Place 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: start in composition seconds, duration in seconds, source_start in source seconds.

composition.add_camera(name: str) → Camera

Create 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]) → Operator

Create 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] = {}) → Collection

Make 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() → None

Delete this composition and its contents; surviving users drop the link, and one Undo restores the deletion.

Layer

A layer of a composition.

layer.name: str attribute

Read the layer name.

layer.remove() → None

Delete 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) → value

Read a layer property at composition time, including animation and live drivers.

Units: time in composition seconds.

layer.set(property: str, value: value, component_index: int | None = None) → None

Set a registered layer property. Target references use a layer or clip id; an empty string uses the fixed target position.

Units: value in the property's displayed units.

layer.id() → str

Read the layer id for property references.

layer.position: gp.Vec3 attribute, settable

Read 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.Vec3 attribute, settable

Read 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.Vec3 attribute, settable

Read 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.Vec3 attribute, settable

Read 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() → str

Read the clip id for property references.

clip.name: str attribute

Read the clip's display name.

clip.kind: str attribute

Read the clip kind.

clip.start: float attribute

Read the start in composition seconds.

clip.duration: float attribute

Read the duration in composition seconds.

clip.layer: Layer attribute

Read 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) → value

Read a clip property's evaluated value at composition time in its displayed units, including animation and live drivers.

Units: time in composition seconds.

clip.set(property: str, value: value, component_index: int | None = None) → None

Set a clip property in its displayed units.

Units: value in the property's displayed units.

clip.rename(name: str) → None

Change a text clip's displayed name, which is its editable text; other clip kinds have no separate native name.

clip.move(start: seconds) → None

Move the clip to a composition time in seconds.

Units: start in composition seconds.

clip.trim(start: seconds, duration: seconds) → None

Replace the clip's composition time range in seconds.

Units: start in composition seconds, duration in seconds.

clip.remove() → None

Delete 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) → None

Replace 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: style in pixel lengths and text box pixel coordinates, runs in Unicode code point ranges; style lengths and coordinates in pixels.

clip.position: gp.Vec3 attribute, settable

Read 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.Vec3 attribute, settable

Read 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.Vec3 attribute, settable

Read 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.Vec3 attribute, settable

Read 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) → value

Read a camera property at composition time, including animation and live drivers.

Units: time in composition seconds.

camera.set(property: str, value: value, component_index: int | None = None) → None

Set a registered camera property. Target references use a layer or clip id; an empty string uses the fixed target position.

Units: value in the property's displayed units.

camera.name: str attribute

Read the camera name.

camera.remove() → None

Delete 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.Vec3 attribute, settable

Read 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.Vec3 attribute, settable

Read 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.Vec3 attribute, settable

Read 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() → str

Read this property's stable id.

property.name: str attribute

Read this property's display name.

property.type: str attribute

Read this property's value type.

property.animatable: bool attribute

Read whether this property accepts keyframes or a live driver.

property.value_at(time: seconds, component_index: int | None = None) → value

Read this property's evaluated value at composition time in its displayed units, including animation and live drivers.

Units: time in composition seconds.

property.set_keys(keys: list[(seconds,value,interpolation)], tangents: list[{incoming:(seconds,value),outgoing:(seconds,value)}] = None, component_index: int | None = None) → None

Replace 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: keys in composition seconds and the property's displayed value units, tangents in seconds and displayed value offsets.

property.set_expression(program: PythonProgram, inputs: dict[str,Property], component_index: int | None = None) → None

Set 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) → None

Choose 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) → None

Clear this property's animation; a property without animation is unchanged.

Asset

An imported media file.

asset.name: str attribute

Read the admitted media asset's name.

asset.rename(name: str) → None

Rename this asset while keeping its media and every use.

asset.remove() → None

Delete 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: time in composition seconds.

Operator

A live Python controller made by add_operator.

operator.remove() → None

Delete this live controller and its owned output bindings; surviving users drop the link, and one Undo restores the deletion.