---
title: Desktop apps
eyebrow: Setting up
description: "Drive Blender, After Effects, Premiere Pro, Illustrator, Photoshop and DaVinci Resolve Studio on your own computer through the NOLGIA plugin: find the connected app, send commands, read their results, and handle the person's approvals."
---

# Desktop apps

The NOLGIA plugin for a desktop app connects the app on your computer to NOLGIA. Once it is signed in and **Connected** is on, you, NOLGIA Agent or any agent holding your token can send it commands: read what is open, run code in the app's own scripting language, render a preview, import a file from your NOLGIA library, export, save, open, and for some apps run the DIT or transcribe clips. The plugin runs each command on your computer and posts the result back.

- Install a plugin from [nolgia.ai/plugins](https://nolgia.ai/plugins): Blender, After Effects, Premiere Pro and Illustrator (one extension for all three), Photoshop, DaVinci Resolve Studio.
- Only your own credentials see your apps and send them commands. NOLGIA Agent reaches an app only while that app's **Allow NOLGIA Agent** switch is on.
- Commands are free. They cost no credits; generations you make along the way are priced as usual.
- With NOLGIA's MCP server, the same commands are the `nolgia_app_*` tools ([MCP](./mcp.html)). This page is the REST API underneath them.

## Use the plugin, never outside scripting

Send every command through the NOLGIA plugin. Never drive the app from outside it: no AppleScript or `osascript`, no COM, no Windows UI automation, no simulated keystrokes or clicks, no second copy of Blender run from a terminal on the person's file, and no `DaVinciResolveScript` from a terminal.

The plugin is what keeps the person in control and their work safe. It lists every command in its **Activity** list, waits for their approval when **Ask before running code** is on, runs code inside one undo step where the app has one, holds back the app's own dialogs where the app allows it so nothing waits for a click, catches every script error and hands it back as text with the failing line. A command sent from outside skips all of that, and a bad call that way can crash the app and lose unsaved work: in testing, one malformed AppleScript command sent straight to After Effects crashed it.

## The endpoints

<!-- gen:endpoints tag=Bridge -->
| Method | Path | What it does |
| --- | --- | --- |
| [GET](../api/#tag/bridge/get/desktop-apps) | `/desktop-apps` | List the desktop apps NOLGIA has a plugin for, with each plugin's status and download. Public, identical for every caller. |
| [GET](../api/#tag/bridge/get/desktop-apps/{app}/rules) | `/desktop-apps/{app}/rules` | Read the rules every agent follows when it works in one desktop app. Public, identical for every caller. |
| [GET](../api/#tag/bridge/get/bridge/sessions) | `/bridge/sessions` | List your desktop apps connected to NOLGIA right now. |
| [POST](../api/#tag/bridge/post/bridge/sessions) | `/bridge/sessions` | Register a desktop app plugin, or check in again (heartbeat). |
| [DELETE](../api/#tag/bridge/delete/bridge/sessions/{id}) | `/bridge/sessions/{id}` | Disconnect a desktop app session. |
| [GET](../api/#tag/bridge/get/bridge/sessions/{id}/next) | `/bridge/sessions/{id}/next` | Long poll for the next command for this app (plugin side). |
| [GET](../api/#tag/bridge/get/bridge/commands) | `/bridge/commands` | List the commands most recently sent to your desktop apps, newest first. |
| [POST](../api/#tag/bridge/post/bridge/commands) | `/bridge/commands` | Send a command to one of your connected desktop apps. |
| [GET](../api/#tag/bridge/get/bridge/commands/{id}) | `/bridge/commands/{id}` | Read a command, optionally waiting for it to finish. |
| [POST](../api/#tag/bridge/post/bridge/commands/{id}/result) | `/bridge/commands/{id}/result` | Report the outcome of a command (plugin side). |
| [POST](../api/#tag/bridge/post/bridge/commands/{id}/cancel) | `/bridge/commands/{id}/cancel` | Cancel a queued or running command. |
<!-- /gen -->

`POST /bridge/sessions`, `GET /bridge/sessions/{id}/next` and `POST /bridge/commands/{id}/result` are what the plugin itself calls; `DELETE /bridge/sessions/{id}` is the plugin switching off. You use the others.

## 1. Find the app

`GET /bridge/sessions` lists the apps connected right now, most recently seen first. A session is live while its plugin has checked in within the last 60 seconds.

```bash
curl -s https://api.nolgia.ai/v1/bridge/sessions -H "Authorization: Bearer $NOLGIA_TOKEN"
# or, with the nolgia CLI:
nolgia api GET /bridge/sessions
```

<!-- gen:fields schema=BridgeSession only=id,app,app_version,plugin_version,machine_name,os,document,capabilities,allow_agent,last_seen_at -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `app` | `BridgeApp` | Yes |  |
| `app_version` | string | Yes |  |
| `plugin_version` | string | Yes |  |
| `machine_name` | string | Yes | The computer's name, as the plugin reports it. |
| `os` | `BridgeOS` | No |  |
| `document` | `BridgeDocument` | Yes |  |
| `capabilities` | array of string | Yes | Command kinds the plugin runs, as it listed them. `info` and `run` are implied, listed or not. |
| `allow_agent` | boolean | Yes | False when the person switched off "Allow NOLGIA Agent" in the plugin; the NOLGIA Agent's commands are then refused. |
| `last_seen_at` | string | Yes | The plugin's latest check in. The session is live while this is within 60 seconds. |
<!-- /gen -->

An empty list means no app is connected: install the plugin, sign in and turn on **Connected**. `capabilities` lists the command kinds that plugin runs; `info` and `run` are always there.

## 2. Send a command

`POST /bridge/commands` queues a command for one app. Without `session_id` it goes to the app's most recently seen live session; pass `session_id` when two windows of the same app are connected.

<!-- gen:fields schema=CreateBridgeCommandRequest -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `app` | `BridgeApp` | Yes |  |
| `session_id` | string | No | A live session of `app` to send it to. Default is the most recently seen live one. |
| `kind` | string | Yes | `info`, `run`, or one of the session's `capabilities`. |
| `args` | object | No | The command's arguments, at most 256 KB as JSON, each `kind` its own (https://docs.nolgia.ai/guides/desktop-apps.html). Default `{}`. |
| `timeout_seconds` | integer | No | Seconds until the command expires if the app has not finished it. |
<!-- /gen -->

```bash
curl -s https://api.nolgia.ai/v1/bridge/commands \
  -H "Authorization: Bearer $NOLGIA_TOKEN" -H "Content-Type: application/json" \
  -d '{"app": "blender", "kind": "run", "args": {"language": "python", "code": "import bpy\nresult = [o.name for o in bpy.data.objects]"}}'
# or: nolgia api POST /bridge/commands --body @command.json
```

The answer is the command, `201` and `queued`. Refusals carry a `code`:

| Status | `code` | Meaning |
| --- | --- | --- |
| 409 | `app_not_connected` | No live session of `app`, or the named session is not live. |
| 422 | `language_not_supported` | A `run` in a language the app does not run. |
| 422 | `capability_not_supported` | The plugin does not run this `kind`: update it. |
| 403 | `agent_not_allowed` | NOLGIA Agent sent it and **Allow NOLGIA Agent** is off. |
| 429 | | More than 120 commands in a minute. |

`args` is at most 256 KB as JSON. `timeout_seconds` (5 to 900, default 120) sets `expires_at`: give a command that waits for the person's approval, a long render or a DIT offload more time.

## 3. Read the result

`GET /bridge/commands/{id}?wait=25` holds the request until the command finishes or 25 seconds pass, then answers with the command as it is. Ask again until `status` is no longer `queued` or `running`.

```bash
while :; do
  cmd=$(nolgia api GET /bridge/commands/$ID --query wait=25)
  status=$(printf '%s' "$cmd" | python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])')
  case "$status" in queued|running) ;; *) printf '%s\n' "$cmd"; break ;; esac
done
```

| `status` | Meaning |
| --- | --- |
| `queued` | Waiting for the plugin to pick it up, normally within moments. |
| `running` | The app is running it, or the plugin is waiting for the person's approval. |
| `succeeded` | Done: `result` holds what the plugin returned. |
| `failed` | It did not work, or the person clicked **Deny**: `error` says which, and for `run` the line that failed. |
| `expired` | `expires_at` passed first, or the app disconnected. |
| `cancelled` | You cancelled it. |

`POST /bridge/commands/{id}/cancel` cancels a queued or running command. The app may already have made changes; cancelling does not undo them. `GET /bridge/commands` lists recent commands (never their `args`, because a `run` carries code).

## Command kinds

| `kind` | `args` | Apps |
| --- | --- | --- |
| `info` | none | All. What is open: the document or project, its comps, sequences, layers, scenes or timelines, the selection. Start every piece of work with it. |
| `run` | `code` (required), `language` (optional, the app's own), `timeout_seconds` | All. Code in the app; see below. |
| `preview` | `width` (16 to 1920); Blender `camera`, `frame`, `engine`; After Effects `comp` and `frame`; Premiere Pro `sequence` and `frame`; Illustrator `artboard`; Photoshop `document`, `region`; DaVinci Resolve `timeline`, `frame` | All. A picture of the document, saved to your NOLGIA library. |
| `import_asset` | `asset_id` (a NOLGIA library asset), `as` (Blender `plane`, `texture` or `clip`; After Effects `footage` or `layer`; Premiere Pro `clip`; Illustrator `embed` or `link`; Photoshop `layer`, `pixels` or `document`); `color_preset` (a NOLGIA color preset slug, installed as a LUT: DaVinci Resolve, Premiere Pro, After Effects); DaVinci Resolve also `asset_ids`, `project_id`, `bin`, `append`, `apply_to`, `node` | All. |
| `export` | `format`: Blender `blend`, `png`, `mp4`, `glb`; After Effects `mp4`, `png`, `aep`; Premiere Pro `mp4`, `png`, `prproj`; Illustrator `png`, `svg`, `pdf`, `ai`; Photoshop `png`, `jpg`, `psd`; DaVinci Resolve `png`, `mp4`. Also `frames` (`24` or `1-120`) and `filename` | All. Pictures and video go to your NOLGIA library; project-file formats make a safety copy next to the file. |
| `save` | `path` (optional; the open file by default). DaVinci Resolve takes none: it saves the open project. | All. |
| `open` | `path`; DaVinci Resolve `project` and `folder` | All. |
| `dit` | `action` (`start`, `status`, `cancel`), `subcommand` (`offload`, `verify`, `manifest`, `inventory`, `rename`, `undo`, `report`), `sources` (one path per camera card), `destinations`, `shoot`, `dry_run`, `dir`, `apply` and the rest of [`nolgia dit`](./cli.html) | After Effects, Premiere Pro, Illustrator, DaVinci Resolve. Runs the NOLGIA CLI on the person's computer. |
| `transcribe` | `action`, `clips`, `sequence`, `engine`, `allow_credits`, `language`, `words` | Premiere Pro. |

A plugin checks `args` before it runs anything and answers a wrong one with an error that names the right choices. `GET /desktop-apps/{app}/rules` serves the rules every NOLGIA workflow in that app follows (safety copies, keeping work editable, previews, the scripting traps), and they apply to your own commands just as well.

## Code in each app

`run` carries code in the app's own language. Set `result` to hand data back; the plugin returns `{value, stdout, stderr}`, and on failure the error with the line that failed.

| App | `language` | What the code sees |
| --- | --- | --- |
| Blender | `python` | `bpy` and the open file. Each run is one undo step. |
| DaVinci Resolve Studio | `python` | `resolve`, `project_manager`, `project`, `media_pool`, `timeline` (None when no timeline is open) and `fusion`. Resolve objects in `result` come back as their names. |
| After Effects, Premiere Pro, Illustrator | `extendscript` | The app's ExtendScript DOM. It is ES3: no `let`, `const`, arrow functions or `JSON`. `result`, or the last expression, is returned. In After Effects each run is one undo step. |
| Photoshop | `uxp` | The body of an async function, already inside `core.executeAsModal`: `app`, `doc` (the active document), `core`, `action`, `batchPlay`, `play` (batchPlay that throws on a failed step), `constants`, `imaging`, `fs`, `modal`, `sleep`. Each run is one History step, undone for you when it fails. |

Code cannot be stopped from outside once it runs: after `timeout_seconds` the plugin stops waiting and says so, and a loop that never ends keeps the app busy until it does. Keep each run to one small step.

## The person's approval

Some commands wait in the plugin for the person to click before they run. While they wait the command is `running`.

- **Ask before running code**, a switch in the plugin: every `run` shows its code first and waits for **Run code** or **Deny**.
- **DIT writes** always wait, whatever that switch says: an offload (**Copy and verify**), a rename with `apply` (**Rename files**) and an undo (**Restore names**). A dry run, a verify, an inventory and a report do not.
- **Opening over unsaved changes** in Blender waits too.

A **Deny** ends the command as `failed` with an error such as "The person at After Effects clicked Deny, so this did not run." No answer before `expires_at` ends it as `failed` with "Nobody approved this in After Effects in time, so it did not run." Tell the person to look at the NOLGIA panel before you send a command that waits, give it a long `timeout_seconds`, and never send it again on your own after a Deny.

## One example per app

Each body below goes to `POST /bridge/commands`.

Blender, the objects in the scene:

```json
{"app": "blender", "kind": "run", "args": {"code": "import bpy\nresult = [(o.name, o.type) for o in bpy.context.scene.objects]"}}
```

After Effects, the comp in the viewer:

```json
{"app": "after_effects", "kind": "run", "args": {"code": "var c = app.project.activeItem;\nresult = (c && c instanceof CompItem) ? {name: c.name, layers: c.numLayers, fps: c.frameRate} : null;"}}
```

Premiere Pro, the sequences in the project:

```json
{"app": "premiere", "kind": "run", "args": {"code": "var out = [];\nfor (var i = 0; i < app.project.sequences.numSequences; i++) out.push(app.project.sequences[i].name);\nresult = out;"}}
```

Illustrator, the open document and its artboards:

```json
{"app": "illustrator", "kind": "run", "args": {"code": "result = app.documents.length ? {name: app.activeDocument.name, artboards: app.activeDocument.artboards.length} : null;"}}
```

Photoshop, the layers of the active document:

```json
{"app": "photoshop", "kind": "run", "args": {"code": "result = doc ? doc.layers.map(l => ({ id: l.id, name: l.name, visible: l.visible })) : null;"}}
```

DaVinci Resolve Studio, the project and its current timeline:

```json
{"app": "resolve", "kind": "run", "args": {"code": "result = {'project': project.GetName(), 'timeline': timeline.GetName() if timeline else None}"}}
```

A preview of the Blender scene at frame 1:

```json
{"app": "blender", "kind": "preview", "args": {"frame": 1, "width": 768}}
```

A DIT plan in Premiere Pro, one path per card, nothing copied:

```json
{"app": "premiere", "kind": "dit", "args": {"action": "start", "subcommand": "offload", "sources": ["/Volumes/A001", "/Volumes/B001"], "destinations": ["/Volumes/RAID/Shoots", "/Volumes/Shuttle"], "shoot": "TEST", "dry_run": true}, "timeout_seconds": 300}
```

The offload itself is the same body without `dry_run`: it waits for **Copy and verify** in the panel and hands back a `job_id` to follow with `"action": "status"`.
