A toolkit for building Adobe extensions.
This is a bit of a rant about how terrible it is to build plugins for Photoshop, and what I've done to make developers' lives easier (mostly mine for now).
Some background about building Adobe plugins
Back in 1999, Adobe created an extensibility platform based on a custom Ecmascript 3 fork called ExtendScript. You had to interact with the application using a strange API called "ActionManager". This was not well documented, so you usually enabled logs, did an action manually in Photoshop (like deleting a layer or opening a file), and the Photoshop-generated log file in your Desktop folder would include the code you needed to use to do the same thing.
For example, the code to select a layer looked something like this:
var idslct = charIDToTypeID( "slct" );
var desc34 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref13 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
ref13.putName( idLyr, "Layer 1" );
executeAction( idslct, desc34, DialogModes.NO );
This is bad for a lot of reasons:
- It could be written much shorter
- In the generated code, the layer is selected by name. What if there are multiple layers with the same name?
- There are 3 "type" systems for keys and values (runtime id, string id, char id)
- There is barely any documentation online
- It's very unergonomic to write
You can imagine it could just have looked like this:
executeAction(
"select",
{ ref: { type: "layer", name: "Layer 1" } },
DialogModes.NO
);
I cannot stress enough how poorly documented these methods are. Google will not help you find anything you need, you'll have to figure it out yourself and do a lot of trial and error. The generated code almost never gives you what you need.
Then, Adobe added CEP (Common Extensibility Platform), which embedded Chromium in Photoshop and allowed you to write extensions in normal JavaScript and use your own UI framework. However, to interact with the application, you still had to use the ActionManager API, but they also introduced some kind of DOM API:
// app is a global object
const documentName = app.activeDocument.name;
for (const layer of app.activeDocument.layers) {
console.log(layer.name);
}
But to do many things, you still needed to use the ActionManager API, now using app.executeAction or app.executeActionGet.
Back in 2017, I made two repos that generated TypeScript definitions for this new DOM API:
and
.
However, the developer experience was still very poor.
Then, in 2020, Adobe created a new system called UXP (Unified Extensibility Platform), with a new DOM API.
But same as with CEP, you still need to use the ActionManager API for many things, however they got rid of the different id systems, made it asynchronous and called it "batchPlay":
const { action } = require("photoshop");
await action.batchPlay([
{
_obj: "select",
_target: [{ _ref: "layer", _id: 1 }]
},
]);
On the ActionManager API side, that's a big ergonomic improvement.
The DOM API is more complete, but it still has a lot of limitations, is poorly documented, and has wrong TypeScript definitions (so I built
).
And to make it worse, it's just a thin wrapper around a synchronous version of
batchPlay. So the internal Photoshop binary APIs are still the same as they were so many years ago. This is noticable because looping over layers and listing their properties can take multiple seconds when using the DOM API, as every
layer.name or
layer.layers actually calls the Photoshop API and blocks the main thread while doing that.
I remember being hopeful when UXP was released, thinking "finally writing Photoshop plugins will actually become fun". But after 2022, it seems like they pulled the plug on the whole thing: documentation is not updated anymore, issues are not fixed, and barely any features are added.
The biggest problem of all is that it's so difficult to figure out what you actually need to do to get a certain result. In the end, you end up building your own badly tested library of functions that will probably break in edge cases, or you end up recording Actions (macros) in Photoshop, and bundling them with your plugin, because you don't know how to write it as code.
Do you want to get a tree of layers? We only figured it out this year. Do you want to know if the plugin is visible? Apparently you have to poll the app's state and filter on some obscure array. Do you want to be notified when the active document changes? Figure out yourself which global events can trigger a document change.
I don't think there's some kind of conspiracy between plugin developing companies having developed their own libraries and not wanting Adobe to make it easier to build plugins so people keep coming to them, but sometimes it feels that way.
I really do not understand why Adobe is so hostile to developers that make their software more usable. Compared to e.g. Figma's plugin documentation, Adobe's plugin system is a joke. If Photoshop wasn't so ubiquitously used by designers, we would have switched to another software long ago. But we had to figure out how to build good plugins because it's essential to our business.
Building UXP toolkit
I wanted to fix this mess, because we use custom plugins a lot at BubblyDoo.
- I needed a way to write tests
- I needed TypeScript definitions
- I needed a library of commonly used, well-tested commands
- I needed some kind of CLI
Making ActionManager/batchPlay manageable
One thing I've learned working with bad APIs:
is essential. The output and input of the ActionManager APIs can be very unpredictable, so I built a layer around it using Zod:
const getLayerCommand = createCommand({
modifying: true,
descriptor: {
_obj: 'get',
_target: [
{
_ref: 'layer',
_id: layerRef.id,
},
{
_ref: 'document',
_id: layerRef.docId,
},
],
},
schema: z.object({
name: z.string(),
}),
});
// { name: "Layer 1" }
// v
const layer = await batchPlayCommand(getLayerCommand);
This makes sure we can interact with Photoshop in a type-safe way.
I've built a library of commonly used commands, and also the helper functions to create your own commands and parse their output.
Building a CLI
I ploughed through
, which is a bit of a mess and took a lot of time to set up. I forked it back in 2023 and I'm publishing a fixed version to
@adobe-fixed-uxp on npm.
It wasn't working perfectly so in 2026 I did some bugfixes, deleted quite some code and migrated it to TypeScript using Claude.
Based on this package, I created a new CLI. For example, this command will open the Photoshop DevTools using Chrome's built-in DevTools:
pnpm --allow-build=@adobe-fixed-uxp/uxp-devtools-helper dlx @bubblydoo/uxp-cli open-devtools
Building a testing framework
Firstly I built a plugin specifically to run tests in Photoshop, but I really wanted to use Vitest.
I imagined connecting to Photoshop using Chrome DevTools Protocol (CDP), which they use in UXP DevTools, and using it to execute the tests inside of it.
It's not obvious how to get connect to Photoshop's CDP endpoint, but I realized it's actually part of the open source UXP DevTools project. You need to use a proprietary Adobe binary (using a communication system called Vulcan) which lets Photoshop connect to a port you specify. Once that connection is set up, you can get a CDP WebSocket endpoint.
With the CDP connection established, I created a custom Vitest pool that can run tests across CDP. It's not perfect, but it works.
Building a library of commonly used commands and functions
Firstly, I created all the helpers needed to be able to build this library. The toolkit provides type-safe wrappers for executeAsModal, batchPlay, etc.
Using the testing framework, I'm building a library of commonly used commands and functions, like "merge layers", "get the layer tree", "delete files in a folder", "copy to clipboard", etc.
You can find them in the repo, but I still need to document them.
Getting the TypeScript definitions
Based on my crawler, Adobe's types, and the work of other independent developers, I corrected the TypeScript definitions and published them to @adobe-uxp-types/photoshop and @adobe-uxp-types/uxp on npm. Then I did a lot of corrections with Claude. Now, the types are manually curated and also part of the UXP toolkit project.
React integration
I also made an integration for React and React Query: @bubblydoo/uxp-toolkit-react. React Query is perfect for these async queries and operations that are so common in Photoshop plugins. There are helpers like useIsPluginPanelVisible or useActiveDocument.
Photoshop MCP server
I built an MCP server that talks to Photoshop by writing code. It has access to the TypeScript definitions and the library of commands and functions, and it's pretty powerful. The code is built using esbuild and then sent to Photoshop over CDP.
pnpm --allow-build=@adobe-fixed-uxp/uxp-devtools-helper dlx @bubblydoo/photoshop-mcp
Future
I hope other developers find it useful and contribute to it. I'm not a full-time Photoshop developer at all, so I'm not sure how much I'll be able to maintain it.