Module 6 β’ Lesson 1π» JavaScript / UXP Scripting in Depth
Actions replay fixed steps; scripts think. With a little JavaScript you can loop over files, branch on logic, do math, and reach parts of Photoshop no Action can touch. You don't need to be an engineer β you need to read, adapt, and run code, and this lesson makes that concrete.
π What You'll Learn
By the end of this lesson, you will be able to:
- Explain the Photoshop DOM: app β documents β layers β properties
- Tell ExtendScript (.jsx) from modern UXP JavaScript, and when each applies
- Write loops and conditionals β the logic Actions can't express
- Read and adapt a complete script, and run
.jsxor UXP.psjsfiles via File βΈ Scripts βΈ Browse - Reach for batchPlay when the DOM doesn't expose something
β±οΈ Estimated Time: 60 minutes
π― Project: One adapted, working script that does something an Action can't β a per-file decision or a computed output.
In This Lesson
π€ The Goal: Steps That Think
An Action does the same thing every time; a script decides. On the right, an Action replays fixed steps. Its one built-in branch (Insert Conditional, such as Document Is Landscape) can only choose which Action to play; it can't count layers, do math, or decide anything else per file. On the left, a script loops, checks conditions, and produces different, correct output per file. Logic is the whole difference. Drag the handle:
π§ Mental Model: The DOM
A script drives Photoshop through its Document Object Model β a tree of objects you read and change. At the top is app; it has documents; each document has layers, a width, a color mode; each layer has a name, opacity, blend mode, bounds. Scripting is just walking that tree and setting properties: "for each document, for each layer, if the name starts with 'BG', hide it." If you can describe the task as objects and properties, you can script it.
βοΈ ExtendScript vs UXP
Two scripting worlds coexist. ExtendScript (.jsx) is the legacy engine: old-JavaScript, a mature DOM, run instantly via File βΈ Scripts βΈ Browse β perfect for quick automation and the vast library of existing scripts online. UXP is the modern platform: current JavaScript and async APIs. It comes in two forms: standalone UXP scripts (.psjs, Photoshop 23.5+) that also run from File βΈ Scripts βΈ Browse, and plugins with real HTML/CSS panels (Lesson 6.2). For a one-off batch job, a .jsx you find online is often the fastest path; for new code you'll maintain, write a .psjs; for a tool with a UI, build a UXP plugin.
.jsx or .psjs immediately, the fastest way to run and test automation. The built-ins above (Image Processor, Load Files into Stack) are scripts too. Export Layers to Files now lives under File βΈ Export βΈ Layers to Files.β οΈ Copy-adapt before you write from scratch
You rarely start with a blank file. The web is full of Photoshop scripts for almost any task; the real skill is reading one, changing the values and logic to fit, and testing on a copy. Treat scripting like LUTs or brushes β a library to draw from, not a language to memorize.
π§Ύ A Script That Loops & Decides
Here is a complete, readable ExtendScript that does something no Action can: loop every open document, branch on its orientation, and export each at the right crop and multiple sizes. Read it top to bottom β you can already follow every line.
// smart-export.jsx (File βΈ Scripts βΈ Browse to run)
var sizes = [2000, 1000, 500]; // long-edge sizes to export, in pixels
var outDir = new Folder("~/Desktop/export");
if (!outDir.exists) outDir.create();
var oldUnits = app.preferences.rulerUnits; // remember the user's units...
app.preferences.rulerUnits = Units.PIXELS; // ...so all the math below is in pixels
for (var d = 0; d < app.documents.length; d++) {
var doc = app.documents[d];
var w = doc.width.value, h = doc.height.value;
var ratio = (w === h) ? 1 : (w > h ? (16/9) : (4/5)); // a decision Actions can't make
for (var s = 0; s < sizes.length; s++) {
var copy = doc.duplicate(); // work on a throwaway copy
copy.convertProfile("sRGB IEC61966-2.1", Intent.RELATIVECOLORIMETRIC, true, false); // web color
copy.bitsPerChannel = BitsPerChannelType.EIGHT; // JPEG can't hold 16-bit
cropToRatio(copy, ratio);
var scale = sizes[s] / Math.max(copy.width.value, copy.height.value);
copy.resizeImage(Math.round(copy.width.value * scale), Math.round(copy.height.value * scale));
var name = doc.name.replace(/\.[^.]+$/, "") + "_" + sizes[s] + ".jpg";
var opt = new JPEGSaveOptions(); opt.quality = 10;
copy.saveAs(new File(outDir + "/" + name), opt, true);
copy.close(SaveOptions.DONOTSAVECHANGES);
}
}
app.preferences.rulerUnits = oldUnits; // put the user's units back
alert("Exported " + app.documents.length + " docs Γ " + sizes.length + " sizes.");
function cropToRatio(doc, r) { // reusable helper
var w = doc.width.value, h = doc.height.value;
if (w / h > r) { var nw = h * r; doc.crop([ (w-nw)/2, 0, (w+nw)/2, h ]); }
else { var nh = w / r; doc.crop([ 0, (h-nh)/2, w, (h+nh)/2 ]); }
}
Two lines at the top matter more than they look. ExtendScript measures in whatever ruler units the user has set, so a script that assumes pixels breaks on a machine set to inches; saving and restoring rulerUnits makes it portable. And because this course's masters are 16-bit and wide-gamut, each copy is converted to sRGB and 8-bit before the JPEG save, which JPEG requires.
The same idea in UXP: a .psjs script
Since Photoshop 23.5, UXP scripts saved with the .psjs extension also run from File βΈ Scripts βΈ Browse, no plugin required. The code is modern JavaScript, the API is asynchronous, and anything that changes a document must run inside core.executeAsModal(), which gives your script exclusive control while it works. Here is the "hide every layer starting with ref" rule as a UXP script:
// hide-ref-layers.psjs (File βΈ Scripts βΈ Browse, Photoshop 23.5+)
const { app, core } = require("photoshop");
async function hideRefLayers() {
const doc = app.activeDocument;
let hidden = 0;
for (const layer of doc.layers) { // top-level layers only
if (layer.name.startsWith("ref")) {
layer.visible = false;
hidden++;
}
}
return hidden;
}
// Anything that edits a document must run inside executeAsModal.
core.executeAsModal(hideRefLayers, { commandName: "Hide ref layers" })
.then(n => app.showAlert(`Hid ${n} layer(s).`))
.catch(e => app.showAlert("Failed: " + e.message));
Compare it with the ExtendScript above: the same DOM idea (app β document β layers β properties), with modern syntax such as const, forβ¦of, and template strings, plus the executeAsModal wrapper. That wrapper is the one UXP rule you can't skip.
β When the DOM can't, batchPlay can
Some features aren't exposed as tidy DOM properties. Both engines let you replay a recorded low-level command β Action Descriptors in ExtendScript, batchPlay in UXP β captured with the ScriptingListener plugin, the free third-party Alchemist UXP plugin, or, in recent versions, the Actions panel: record a step, right-click it, and choose Copy As JavaScript to get ready-made batchPlay code. It's less pretty, but it means anything you can do in the UI, you can script.
π οΈ Guided Build: Adapt a Script
Start from the script above (or any script you find) and make it yours.
Step 1: Run it as-is Β· 8 min
- Paste the script into a plain-text or code editor (VS Code with Adobe's ExtendScript Debugger extension works well; the old ExtendScript Toolkit is discontinued). Open a few copies of documents, save the script as
smart-export.jsx, and run it via File βΈ Scripts βΈ Browse. Confirm the export folder fills up.
Step 2: Change the values Β· 8 min
- Edit the
sizesarray and the JPEG quality. Re-run and confirm your changes took effect. You're editing behavior without writing anything from scratch.
Step 3: Add logic Β· 12 min
- Add a condition: skip documents already under a certain size, or add a ratio for panoramas (anything more than twice as wide as it is tall gets 2:1 instead of 16:9). One
ifis a capability an Action simply lacks.
Step 4: Make it safe Β· 6 min
- Wrap the work in
try/catchso one bad file doesn't halt the run, and log failures. Thefinallyblock closes the copy even after an error; a leftover copy would otherwise joinapp.documentsand be processed too. Now it's a tool, not a toy. π
var log = []; // before the loop
// inside the per-size loop:
var copy = null;
try {
copy = doc.duplicate();
/* ...the convert / crop / resize / save work... */
} catch (e) {
log.push(doc.name + " @" + sizes[s] + ": " + e.message); // note it and keep going
} finally {
if (copy) copy.close(SaveOptions.DONOTSAVECHANGES); // never leave a copy open
}
// after the loop:
if (log.length) {
var f = new File(outDir + "/errors.txt");
f.open("w"); f.write(log.join("\n")); f.close();
}
β Project Completion Checklist
- β Ran an existing script successfully via File βΈ Scripts βΈ Browse (and read the
.psjsexample) - β Edited its values and confirmed the change
- β Added a conditional β logic an Action can't do
- β Wrapped it in try/catch with error logging
- β Understand where batchPlay/descriptors fit
π§ Now You: Solo Variation
π Your challenge
- Write a script that renames or hides layers by a rule (e.g., hide every layer whose name starts with "ref").
- Adapt an "export each layer as a PNG" script you find online, and add a filter so it only exports visible layers.
- Record a UI action with the ScriptingListener, then paste the descriptor into a script β your first taste of scripting the un-exposed.
Going further: read Adobe's UXP scripting docs and port one of your ExtendScripts to UXP JavaScript. The concepts transfer; the syntax modernizes.
π³ Recipe Card: Scripting
Walk the tree, add the logic
- Think in the DOM: app β documents β layers β properties
- ExtendScript (.jsx) for quick jobs; UXP
.psjsscripts (insideexecuteAsModal) and plugins for modern tools - Use loops + conditionals for what Actions can't do
- Copy-adapt existing scripts; test on copies
- batchPlay/descriptors for un-exposed commands; wrap in try/catch
Mantra: if you can describe it as objects and decisions, you can script it.
π Learning Journal
Add to your journal after this lesson:
- Key concepts you learned
- Techniques that clicked for you
- Questions or confusion points to revisit
- Ideas you want to try
- Your progress and feelings about learning this
βοΈ This lesson's prompt: What repetitive task in your workflow needs a decision Actions can't make? That's your first real script β describe it as objects and conditions.
π Lesson Summary
π Key Takeaways
- Scripts drive Photoshop through the DOM:
appβ documents β layers β properties. - ExtendScript (.jsx) is legacy but instant; UXP is modern, as
.psjsscripts or full plugins. - Both run from File βΈ Scripts βΈ Browse; UXP edits must run inside
core.executeAsModal(). - Make scripts portable and safe: set
rulerUnitsand restore it, convert to 8-bit sRGB before JPEG, and wrap work intry/catchwith a log. - batchPlay and Action Descriptors reach anything the DOM doesn't expose.
π What You've Accomplished
You ran, edited, and extended a real script that makes decisions no Action can, then made it robust enough to trust on a folder of client files. You also saw the same idea in modern UXP, which is the foundation for the panel you build next.
β Common Questions at This Stage
Do I have to learn "real" programming for this?
Not to be effective. Reading, adapting, and running scripts covers most professional needs and is a learnable-in-a-weekend skill. Deeper programming unlocks more, but the return on "can adapt a script" is enormous on its own.
ExtendScript is old. Should I ignore it?
No. It's mature, instantly runnable, and backed by a huge library of existing scripts, so it's often the fastest way to solve a one-off. Write new code you'll maintain in UXP, but ExtendScript is still a productive tool today.
My UXP script throws an error about modal state. What's wrong?
You changed a document outside core.executeAsModal(). Move every edit into the function you pass to executeAsModal; reading values is fine outside it, changing them is not.
π Looking Ahead
A script runs from a menu; a tool has a face. Next: Lesson 6.2: Building a Panel / Plugin, where UXP turns your scripts into a real HTML/CSS panel with buttons, docked inside Photoshop.
β Before the Next Lesson
- Keep your adapted
smart-export.jsxwith its try/catch log; you'll reuse it. - Run the
.psjsexample on a test file and change the rule it uses. - Write your Learning Journal entry.
π Additional Resources
π Encouragement for the Journey
The first script that saves you an afternoon changes how you see every repetitive task. You don't have to become an engineer; you just proved you can read, adapt, and trust code, and that's the skill that scales.