Embed SDK Workflow Tethering tutorial
Learn how to combine multiple workflows to create a more complex experience with the Adobe Express Embed SDK.
Introduction
Welcome! This advanced hands-on tutorial will walk you through implementing workflow tethering that seamlessly connects the Generate Image, Edit Image, and Full Editor experiences.
By the end of this tutorial, you'll have built a complete workflow tethering system where users can generate images, edit them with basic tools, and seamlessly transition to the full Adobe Express editor for advanced editing capabilities.
What you'll learn
By completing this tutorial, you'll gain practical skills in:
- Implementing workflow tethering between multiple Adobe Express experiences.
- Building a modular architecture for complex SDK integrations.
- Managing intent transitions and export configurations.
- Creating reusable workflow components.
What you'll build
You'll build a web application that demonstrates two key workflow tethering patterns:
- Generate Image → Edit Image: Users generate images and seamlessly transition to basic editing
- Edit Image → Full Editor: Users performing basic edits can access advanced editing capabilities
Prerequisites
data-variant=warning
data-slots=text1, text2
- Workflow Tethering guide: Comprehensive guide to understanding workflow tethering.
- Edit Image tutorial: Understanding of Edit Image workflow. implementation.
- Generate Image tutorial: Knowledge of Generate Image workflow.
Additionally, make sure you have:
- An Adobe account and API credentials from the Adobe Developer Console
- Basic knowledge of HTML, CSS, and JavaScript
- Understanding of ES6 modules and async/await patterns
- Node.js installed on your development machine (v20.19.0 or higher)
- A text editor or IDE of your choice
1. Set up the project
1.1 Clone the sample
Clone the Embed SDK Workflow Tethering sample from GitHub and navigate to the project directory.
git clone https://github.com/AdobeDocs/embed-sdk-samples.git
cd embed-sdk-samples/code-samples/tutorials/embed-sdk-workflow-tethering
The project features a modular architecture with this structure:
.
├── package.json 📦 Project configuration
├── vite.config.js 🔧 Build configuration
└── src
├── main.js 🎯 Main orchestration
├── utils/
│ └── shared.js 🔧 Common utilities & state management
├── config/
│ ├── exportConfigs.js 📋 All export button configurations
│ └── appConfigs.js ⚙️ App configuration factory
├── workflows/
│ ├── generateToEdit.js 🖼️ Generate Image → Edit Image workflow
│ └── editToFullEditor.js ✨ Edit Image → Full Editor workflow
├── index.html 🌐 UI container
└── style.css 🎨 CSS styles
1.2 Set up the API key
Locate the src/.env file and replace the placeholder string in the VITE_API_KEY with your Embed SDK API Key:
VITE_API_KEY="your-api-key-here!"
data-variant=info
data-slots=text1
localhost:5555 domain and port.1.3 Install dependencies and run
Install the dependencies and start the development server:
npm install
npm run start
The web application will be served at localhost:5555 on a secure HTTPS connection. Open your browser and navigate to this address to see the workflow tethering in action.
<!--
-->
data-variant=error
data-slots=header, text1
Error: "Adobe Express is not available"
src/.env file as described here.2. Understanding the modular architecture
Compared to the two previous tutorials, we're going to use a modular architecture that separates concerns and makes complex workflow tethering manageable and maintainable.
2.1 Architecture overview
We have organized the code into focused modules:
main.js: UI event coordination and SDK initializationutils/shared.js: State management and common utilitiesconfig/: Configuration objects and export button definitionsworkflows/: Workflow-specific logic and transition handling
The main.js module is the orchestrator of the application.
data-slots=heading, code
data-repeat=1
main.js
// 👇 Image management utilities and state
import { cacheDefaultImageBlob, handleFileSelection, setCurrentWorkflow } from "./utils/shared.js";
// 👇 Button configurations for different workflow starts
import { startGenImageExportConfig, startEditImageExportConfig } from "./config/exportConfigs.js";
// 👇 Workflow-specific configurations
import { createGenerateImageAppConfig, createEditImageAppConfig } from "./config/appConfigs.js";
It initializes the Adobe Express SDK, sets up UI event listeners (button clicks, file selection), and coordinates workflow launches. All the rest is delegated to the specialized modules.
2.2 State management strategy
The utils/shared.js module serves as the single source of truth for application state:
data-slots=heading, code
data-repeat=1
utils/shared.js
// Global state management
// Tracks which workflow initiated the session
export let currentWorkflow = null;
// Caches the image blob for efficient SDK usage
export let currentImageBlob = null;
export function setCurrentWorkflow(workflow) {
currentWorkflow = workflow
}
export function resetWorkflow() {
currentWorkflow = null;
}
export function setCurrentImageBlob(blob) {
currentImageBlob = blob;
}
// DOM element references
export const generateImage = document.getElementById("image1");
export const expressImage = document.getElementById("image2");
// Cache the default image as a blob for efficient SDK usage
export async function cacheDefaultImageBlob() { /* ... */ }
// Handle file selection from file input
export function handleFileSelection(event) { /* ... */ }
// Base callback configurations used across workflows
export const baseCallbacks = {
// Reset workflow tracking on cancel
onCancel: () => { resetWorkflow() },
onError: (err) => {
console.error("Error!", err.toString());
},
};
// Update an image element and cache the blob for future use
export async function updateImageAndCache(
imageElement,
imageData,
updateCache = false
) { /* ... */ }
3. Export configurations and user journeys
One of the most critical aspects of workflow tethering is providing users with contextually appropriate export options at each stage of their journey. These will define the buttons that allow users to either save the image, download it, or continue the experience in the next workflow.
3.1 The four export configuration stages
This sample application defines four distinct export configurations that correspond to different stages in the user's workflow:
data-slots=heading, code
data-repeat=1
config/exportConfigs.js
// 1. Starting with the Generate Image workflow...
export const startGenImageExportConfig = [
{ id: "download", label: "Download", /* ... */ },
{ id: "save-generated-image", label: "Save generated image", /* ... */ },
// 👇 Enables transition
{ id: "open-edit-image", label: "Edit image", /* ... */ },
];
// ... and ending with the Edit Image workflow
export const endEditImageExportConfig = [
{ id: "download", label: "Download", /* ... */ },
{ id: "save-final-image", label: "Save final image", /* ... */ },
];
// 2. Starting with the Edit Image workflow...
export const startEditImageExportConfig = [
{ id: "download", label: "Download", /* ... */ },
{ id: "save-edited-image", label: "Save edited image", /* ... */ },
// 👇 Enables options
{ type: "continue-editing", label: "Do More", /* ... */ },
];
// ... and ending with the Full Editor workflow
export const endFullEditorExportConfig = [
{ id: "download", label: "Download", /* ... */ },
{ id: "save-full-editor-result", label: "Save design", /* ... */ },
];
Each configuration is designed for its specific context:
- Start configurations: Include transition options ("Edit image" and "Do More" buttons)
- End configurations: Focus on completion actions (download, save)
Please note, each workflow can only define its own export configurations. In order to define export configurations for the tethered workflow (the next one), you would need to pass the new export configuration via the onIntentChange callback—as we'll see in Section 4.1.2.
4. Workflow tethering patterns
The application demonstrates two tethering patterns, each implemented as a dedicated module. Let's dive into each one in detail.
4.1 Generate Image → Edit Image workflow
The workflows/generateToEdit.js module handles the transition from the Generate Image to the Edit Image experience.
4.1.1 Initial workflow launch
When users click "Generate Image", the system uses the full-featured configuration.
data-slots=heading, code
data-repeat=1
main.js
//... other imports...
import { startGenImageExportConfig } from "./config/exportConfigs.js";
import { createGenerateImageAppConfig } from "./config/appConfigs.js";
//... Import and initialize the SDK...
// Create the Generate Image workflow appConfig using the factory function
const generateImageAppConfig = createGenerateImageAppConfig();
// Click handler for the Generate Image button
document.getElementById("generateBtn").onclick = async () => {
// Set the current workflow to "generate"
setCurrentWorkflow("generate");
module.createImageFromText(
generateImageAppConfig, // the appConfig
startGenImageExportConfig // the exportConfig
);
};
In main.js, the export configuration object startGenImageExportConfig is imported from config/exportConfigs.js—as we've seen in Section 3.1.
The initial application configuration object generateImageAppConfig is created with the createGenerateImageAppConfig() factory function, imported from config/appConfigs.js. This function returns a workflow-specific configuration object for the Generate Image workflow.
data-slots=heading, code
data-repeat=3
config/appConfigs.js
import { baseCallbacks } from "../utils/shared.js";
import {
createGenerateToEditTransition,
createGenerateImageWorkflowConfig,
} from "../workflows/generateToEdit.js";
// Intent change handler factory
// Manages transitions between different workflows
export function createIntentChangeHandler() {
return (oldIntent, newIntent) => {
console.log("Intent transition:", oldIntent, "→", newIntent);
// Generate Image → Edit Image transition
if (oldIntent === "create-image-from-text") {
return createGenerateToEditTransition();
}
return undefined;
};
}
// Create the app configuration for Generate Image workflow
export function createGenerateImageAppConfig() {
const intentChangeHandler = createIntentChangeHandler();
return createGenerateImageWorkflowConfig(baseCallbacks, intentChangeHandler);
}
workflows/generateToEdit.js
import { endEditImageExportConfig } from "../config/exportConfigs.js";
// ...
export function createGenerateImageWorkflowConfig(
baseCallbacks, onIntentChangeHandler
) {
return {
appVersion: "2",
featureConfig: {
"community-wall": true,
"fast-mode": false,
"custom-models": false,
},
thumbnailOptions: ["rich-preview", "edit-dropdown"],
editDropdownOptions: [
{ option: "add-effects" },
{ option: "remove-background" },
// ... more options
],
callbacks: {
...baseCallbacks,
onPublish: handleGenerateImagePublish,
onIntentChange: onIntentChangeHandler,
},
};
}
utils/shared.js
export let currentWorkflow = null; // 'generate' or 'edit'
export function resetWorkflow() { currentWorkflow = null }
// ... other shared utilities...
// Base callback configurations used across workflows
export const baseCallbacks = {
onCancel: () => {
// Reset workflow tracking on cancel
resetWorkflow();
},
onError: (err) => {
console.error("Error!", err.toString());
},
};
The generateImageAppConfig object creation follows a dependency chain across these multiple files:
-
config/appConfigs.js: thecreateGenerateImageAppConfig()factory function orchestrates the configuration creation:createIntentChangeHandler()is executed to create theonIntentChange()handler for workflow transitions. Internally, this function callscreateGenerateToEditTransition()imported fromworkflows/generateToEdit.js, and returns the export configuration for the Edit Image workflow that is tethered to, i.e., follows after, Generate Image.- It passes both the
baseCallbacksfromutils/shared.jsand theintentChangeHandlertocreateGenerateImageWorkflowConfig()fromworkflows/generateToEdit.js, which returns the complete configuration object for the Generate Image workflow.
-
utils/shared.js: Provides the foundationalbaseCallbacksobject containing theonCancelandonErrorcallbacks, shared across all workflows. -
workflows/generateToEdit.js: ThecreateGenerateImageWorkflowConfig()function builds the complete configuration object by:- Setting Generate Image-specific properties (
appVersion,featureConfig,thumbnailOptions,editDropdownOptions). - Merging the
baseCallbackswith workflow-specific callbacks likeonPublishandonIntentChange. - The
onIntentChangecallback uses the intent change handler to detect transitions between workflows and pass the appropriate export configuration for the tethered workflow.
- Setting Generate Image-specific properties (
4.1.2 Handling the transition
When users click the "Edit image" button in the Generate Image interface to move to the Edit Image experience, the intent change mechanism activates
data-slots=heading, code
data-repeat=2
config/appConfigs.js
export function createIntentChangeHandler() {
return (oldIntent, newIntent) => {
console.log("Intent transition:", oldIntent, "→", newIntent);
// Generate Image → Edit Image transition
if (oldIntent === "create-image-from-text") { // 👈
return createGenerateToEditTransition(); // 👈
} // 👈
return undefined;
};
}
workflows/generateToEdit.js
import { endEditImageExportConfig } from "../config/exportConfigs.js";
// Creates the workflow transition configuration for Generate Image → Edit Image
// This is returned by onIntentChange when transitioning to the Edit Image
export function createGenerateToEditTransition() {
return {
exportConfig: endEditImageExportConfig, // 👈 Different buttons!
};
}
data-variant=info
data-slots=text1
createIntentChangeHandler() function is used to create the onIntentChange() handler for workflow transitions; it returns an object of type IntentChangeConfig, which contains the appropriate exportConfig for the Edit Image workflow that follows after Generate Image.The user is now able to perform Image Editing routines, such as Removing Background, Adding Effects, and more.
4.1.3 State synchronization
The publish callback ensures proper state management, and the image is updated with the generated content.
data-slots=heading, code
data-repeat=1
workflows/generateToEdit.js
// ...
export async function handleGenerateImagePublish(intent, publishParams) {
// Update the left image (image1) with generated content
generateImage.src = publishParams.asset[0].data;
console.log("Updated generateImage (image1) with generated content");
// Reset workflow tracking
resetWorkflow();
}
4.2 Edit Image → Full Editor workflow
The workflows/editToFullEditor.js module manages the transition from the Edit Image to Full Editor experience.
4.2.1 Initial workflow launch
When users click "Edit Image", the Edit Image workflow starts with a minimal export configuration.
data-slots=heading, code
data-repeat=1
main.js
//... other imports...
import { startEditImageExportConfig } from "./config/exportConfigs.js";
import { createEditImageAppConfig } from "./config/appConfigs.js";
//... Import and initialize the SDK...
// Create the Edit Image workflow appConfig using the factory function
const editImageAppConfig = createEditImageAppConfig();
// Click handler for the Edit Image button
document.getElementById("editBtn").onclick = async () => {
// Set workflow tracking
setCurrentWorkflow("edit");
const docConfig = {
asset: {
type: "image",
name: "Demo Image",
dataType: "blob",
data: currentImageBlob, // Use cached blob (default or user-selected)
},
};
module.editImage(docConfig, editImageAppConfig, startEditImageExportConfig);
};
Similarly to the Generate Image workflow, the export configuration object startEditImageExportConfig is imported from config/exportConfigs.js—as we've seen in Section 3.1.
data-slots=heading, code
data-repeat=3
config/appConfigs.js
import { baseCallbacks } from "../utils/shared.js";
import {
createEditToFullEditorTransition,
createEditImageWorkflowConfig,
} from "../workflows/editToFullEditor.js";
// Intent change handler factory
// Manages transitions between different workflows
export function createIntentChangeHandler() {
return (oldIntent, newIntent) => {
console.log("Intent transition:", oldIntent, "→", newIntent);
// Edit Image → Full Editor transition
if (oldIntent === "edit-image-v2") {
return createEditToFullEditorTransition();
}
return undefined;
};
}
// Create the app configuration for Edit Image workflow
export function createEditImageAppConfig() {
const intentChangeHandler = createIntentChangeHandler();
return createEditImageWorkflowConfig(baseCallbacks, intentChangeHandler);
}
workflows/editToFullEditor.js
import { endEditImageExportConfig } from "../config/exportConfigs.js";
// ...
export function createEditImageWorkflowConfig(
baseCallbacks,
onIntentChangeHandler
) {
return {
appVersion: "2",
callbacks: {
...baseCallbacks,
onPublish: handleEditImagePublish,
onIntentChange: onIntentChangeHandler,
},
};
}
utils/shared.js
export let currentWorkflow = null; // 'generate' or 'edit'
export function resetWorkflow() { currentWorkflow = null }
// ... other shared utilities...
// Base callback configurations used across workflows
export const baseCallbacks = {
onCancel: () => {
// Reset workflow tracking on cancel
resetWorkflow();
},
onError: (err) => {
console.error("Error!", err.toString());
},
};
The editImageAppConfig object creation follows a dependency chain across these multiple files:
-
config/appConfigs.js: thecreateEditImageAppConfig()factory function orchestrates the configuration creation:createIntentChangeHandler()is executed to create theonIntentChange()handler for workflow transitions. Internally, this function callscreateEditToFullEditorTransition()imported fromworkflows/editToFullEditor.js, and returns the export configuration for the Full Editor workflow that is tethered to, i.e., follows after, Edit Image.- It passes both the
baseCallbacksfromutils/shared.jsand theintentChangeHandlertocreateEditImageWorkflowConfig()fromworkflows/editToFullEditor.js, which returns the complete configuration object for the Edit Image workflow.
-
utils/shared.js: Provides the foundationalbaseCallbacksobject containing theonCancelandonErrorcallbacks, shared across all workflows. -
workflows/editToFullEditor.js: ThecreateEditImageWorkflowConfig()function builds the complete configuration object by:- Setting the Edit Image
appVersion. - Merging the
baseCallbackswith workflow-specific callbacks likeonPublishandonIntentChange. - The
onIntentChangecallback uses the intent change handler to detect transitions between workflows and pass the appropriate export configuration for the tethered workflow.
- Setting the Edit Image
4.2.2 Handling the transition
When users click the "Do more" button in the Edit Image interface to move to the Full Editor experience, the intent change mechanism activates.
data-slots=heading, code
data-repeat=2
config/appConfigs.js
export function createIntentChangeHandler() {
return (oldIntent, newIntent) => {
console.log("Intent transition:", oldIntent, "→", newIntent);
// Edit Image → Full Editor transition
if (oldIntent === "edit-image-v2") { // 👈
return createEditToFullEditorTransition(); // 👈
} // 👈
return undefined;
};
}
workflows/editToFullEditor.js
import { endFullEditorExportConfig } from "../config/exportConfigs.js";
// Creates the workflow transition configuration for Edit Image → Full Editor
// This is returned by onIntentChange when transitioning to the Full Editor
export function createEditToFullEditorTransition() {
return {
appConfig: {
appVersion: "2",
callbacks: {
...baseCallbacks,
onPublish: handleFullEditorPublish, // 👈 Different callback!
},
},
exportConfig: endFullEditorExportConfig, // 👈 Full Editor export options
};
}
The user is now able to use the entire set of features available in Adobe Express, such as Adding Text or shapes.
4.2.3 Dual state management
This workflow manages two different publish scenarios:
data-slots=heading, code
data-repeat=1
workflows/editToFullEditor.js
// workflows/editToFullEditor.js
// Publish callback for Edit Image workflow
// Updates the edited image and maintains blob cache
export async function handleEditImagePublish(intent, publishParams) {
console.log("Edit workflow - intent:", intent);
console.log("Edit workflow - publishParams:", publishParams);
// Update the right image (image2) with edited content
await updateImageAndCache(expressImage, publishParams.asset[0].data, true);
console.log("Updated expressImage (image2) with edited content");
// Reset workflow tracking
resetWorkflow();
}
// Publish callback for Full Editor workflow
// Handles the final result from the Full Editor
export async function handleFullEditorPublish(intent, publishParams) {
console.log("Full editor workflow - intent:", intent);
console.log("Full editor workflow - publishParams:", publishParams);
// Save back to image2 (expressImage) - same logic as regular edit workflow
await updateImageAndCache(expressImage, publishParams.asset[0].data, true);
console.log("Updated expressImage (image2) with full editor content");
}
This ensures that the image is updated with either the content from the Edit Image workflow or the Full Editor workflow.
5. End-to-end user journeys
Let's trace through the complete user journeys to understand how all the pieces work together.
5.1 Generate Image → Edit Image → Save
-
The user selects Generate Image:
main.jssetscurrentWorkflow = "generate"- SDK launches with
generateImageAppConfig+startGenImageExportConfig
-
The user generates an image
handleGenerateImagePublish()updatesgenerateImage.src- Workflow state resets
-
The user selects Edit image in the Generate Image experience:
onIntentChange()detects"create-image-from-text"as the old intentcreateGenerateToEditTransition()returnsendEditImageExportConfig- SDK transitions to Edit Image with different export buttons
-
The user edits and clicks Save final image
handleEditImagePublish()would be called- Image updates and workflow completes
data-variant=warning
data-slots=header, text1
Tethering to Edit Image v1
5.2 Edit Image → Full Editor → Save
-
The user selects Edit Image (direct entry):
main.jssetscurrentWorkflow = "edit"- SDK launches with
editImageAppConfig+startEditImageExportConfig
-
The user selects Do More → Add text (or other advanced option):
onIntentChange()detects"edit-image-v2"as the old intentcreateEditToFullEditorTransition()returnsendFullEditorExportConfig- SDK launches the Full Editor with different export buttons
-
The user performs advanced editing and clicks Save design
handleFullEditorPublish()updatesexpressImage.srccurrentImageBlobcache updates for future edits- Workflow completes with advanced editing applied
Troubleshooting
Common issues
src/.env file as described here.Complete working example
The complete implementation demonstrates all the concepts covered in this tutorial. You can either find it in the Embed SDK samples repository or below, split in two blocks for convenience.
data-slots=heading, code
data-repeat=3
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Embed SDK Sample</title>
</head>
<body>
<sp-theme scale="medium" color="light" system="express">
<div class="container">
<header>
<h1>Adobe Express Embed SDK</h1>
<sp-divider size="l"></sp-divider>
<h2>Workflows Tethering Sample</h2>
<p>
Click either CTA button to test the tethered experiences.
</p>
</header>
<main class="two-column-layout">
<div class="column">
<h3>Generate Image → Edit Image</h3>
<img id="image1" src="./images/generate-image.png"
alt="Generate Image" />
<sp-button-group>
<sp-button id="generateBtn">Generate Image</sp-button>
</sp-button-group>
<input type="file" id="fileInput1"
accept="image/*" style="display: none;" />
</div>
<div class="column">
<h3>Edit Image → Full Editor</h3>
<img id="image2" src="./images/demo-image-1.jpg"
alt="An Indian woman holding a cat" />
<sp-button-group>
<sp-button id="uploadBtn" treatment="outline">
Choose Image
</sp-button>
<sp-button id="editBtn">Edit Image</sp-button>
</sp-button-group>
<input type="file" id="fileInput"
accept="image/*" style="display: none;" />
</div>
</main>
</div>
</sp-theme>
<script type="module" src="./main.js"></script>
</body>
</html>
main.js
// Import theme and typography styles from Spectrum Web Components
import "@spectrum-web-components/styles/typography.css";
import "@spectrum-web-components/theme/express/theme-light.js";
import "@spectrum-web-components/theme/express/scale-medium.js";
import "@spectrum-web-components/theme/sp-theme.js";
// Import Spectrum Web Components
import "@spectrum-web-components/button/sp-button.js";
import "@spectrum-web-components/button-group/sp-button-group.js";
import "@spectrum-web-components/divider/sp-divider.js";
import "./style.css";
// Import our modular components
import {
cacheDefaultImageBlob,
handleFileSelection,
setCurrentWorkflow,
currentImageBlob,
} from "./utils/shared.js";
import {
startGenImageExportConfig,
startEditImageExportConfig,
} from "./config/exportConfigs.js";
import {
createGenerateImageAppConfig,
createEditImageAppConfig,
} from "./config/appConfigs.js";
// Import the Adobe Express Embed SDK
await import("https://cc-embed.adobe.com/sdk/v4/CCEverywhere.js");
console.log("CCEverywhere loaded", window.CCEverywhere);
// Parameters for initializing the Adobe Express Embed SDK
const hostInfo = {
clientId: import.meta.env.VITE_API_KEY,
appName: "Embed SDK Sample",
};
// Optional parameters - enable delayed login for Generate Image functionality
const configParams = {
loginMode: "delayed",
};
// Initialize the Adobe Express Embed SDK
const { module } = await window.CCEverywhere.initialize(hostInfo, configParams);
// Initialize the application
await cacheDefaultImageBlob();
// Create workflow configurations
const generateImageAppConfig = createGenerateImageAppConfig();
const editImageAppConfig = createEditImageAppConfig();
// ============================================================================
// UI Event Handlers
// ============================================================================
// Click handler for the Choose Image button
document.getElementById("uploadBtn").onclick = () => {
document.getElementById("fileInput").click();
};
// Handle file selection
document.getElementById("fileInput").onchange = handleFileSelection;
// Click handler for the Edit Image button
document.getElementById("editBtn").onclick = async () => {
// Set workflow tracking
setCurrentWorkflow("edit");
const docConfig = {
asset: {
type: "image",
name: "Demo Image",
dataType: "blob",
data: currentImageBlob, // Use cached blob (default or user-selected)
},
// intent: "add-effects", // Optional: specify a particular editing intent
};
module.editImage(docConfig, editImageAppConfig, startEditImageExportConfig);
};
// Click handler for the Generate Image button
document.getElementById("generateBtn").onclick = async () => {
// Set workflow tracking
setCurrentWorkflow("generate");
module.createImageFromText(generateImageAppConfig, startGenImageExportConfig);
};
utils/shared.js
// Global state management
export let currentWorkflow = null; // 'generate' or 'edit'
export let currentImageBlob = null;
export function setCurrentWorkflow(workflow) {
currentWorkflow = workflow;
}
export function resetWorkflow() {
currentWorkflow = null;
}
export function setCurrentImageBlob(blob) {
currentImageBlob = blob;
}
// DOM element references
export const generateImage = document.getElementById("image1");
export const expressImage = document.getElementById("image2");
/**
* Cache the default image as a blob for efficient SDK usage
*/
export async function cacheDefaultImageBlob() {
const response = await fetch(expressImage.src);
currentImageBlob = await response.blob();
}
/**
* Handle file selection from file input
* @param {Event} event - The file input change event
*/
export function handleFileSelection(event) {
const file = event.target.files[0];
if (file && file.type.startsWith("image/")) {
// Dual data flow: cache the File (which is a Blob) for SDK, convert to data URL for display
currentImageBlob = file; // File objects are Blobs - perfect for SDK usage
// Convert to data URL for immediate display in the <img> element
const reader = new FileReader();
reader.onload = (e) => {
expressImage.src = e.target.result; // Base64 encoded image data
};
reader.readAsDataURL(file);
}
}
/**
* Base callback configurations used across workflows
*/
export const baseCallbacks = {
onCancel: () => {
// Reset workflow tracking on cancel
resetWorkflow();
},
onError: (err) => {
console.error("Error!", err.toString());
},
};
/**
* Update an image element and cache the blob for future use
* @param {HTMLImageElement} imageElement - The image element to update
* @param {string} imageData - The image data URL
* @param {boolean} updateCache - Whether to update the global blob cache
*/
export async function updateImageAndCache(
imageElement,
imageData,
updateCache = false
) {
imageElement.src = imageData;
if (updateCache) {
const response = await fetch(imageData);
currentImageBlob = await response.blob();
}
}
data-slots=heading, code
data-repeat=4
config/exportConfigs.js
// When starting Generate Image workflow
export const startGenImageExportConfig = [
{
id: "download",
label: "Download",
action: { target: "download" },
style: { uiType: "button" },
},
{
id: "save-generated-image",
label: "Save generated image",
action: { target: "publish" },
style: { uiType: "button" },
},
{
id: "open-edit-image",
label: "Edit image",
action: { target: "image-module" },
style: { uiType: "button" },
},
];
// When starting Edit Image workflow
export const startEditImageExportConfig = [
{
id: "download",
label: "Download",
action: { target: "download" },
style: { uiType: "button" },
},
{
id: "save-edited-image",
label: "Save edited image",
action: { target: "publish" },
style: { uiType: "button" },
},
{
type: "continue-editing",
label: "Do More",
style: { uiType: "button", variant: "secondary", treatment: "fill" },
options: [
{
id: "exportOption1",
style: { uiType: "dropdown" },
action: { target: "express", intent: "add-text" },
},
{
id: "exportOption2",
style: { uiType: "dropdown" },
action: { target: "express", intent: "add-images" },
},
{
id: "exportOption3",
style: { uiType: "dropdown" },
action: { target: "express", intent: "add-icons-and-shapes" },
},
],
},
];
// When ending Edit Image workflow (after basic edits)
export const endEditImageExportConfig = [
{
id: "download",
label: "Download",
action: { target: "download" },
style: { uiType: "button" },
},
{
id: "save-final-image",
label: "Save final image",
action: { target: "publish" },
style: { uiType: "button" },
},
];
// When ending Full Editor workflow (after advanced edits)
export const endFullEditorExportConfig = [
{
id: "download",
label: "Download",
action: { target: "download" },
style: { uiType: "button" },
},
{
id: "save-full-editor-result",
label: "Save design",
action: { target: "publish" },
style: { uiType: "button" },
},
];
config/appConfigs.js
/**
* App configuration factory
* Creates workflow-specific app configurations for the Adobe Express Embed SDK
*/
import { baseCallbacks } from "../utils/shared.js";
import {
createGenerateToEditTransition,
createGenerateImageWorkflowConfig,
} from "../workflows/generateToEdit.js";
import {
createEditToFullEditorTransition,
createEditImageWorkflowConfig,
} from "../workflows/editToFullEditor.js";
/**
* Intent change handler factory
* Manages transitions between different workflows
*/
export function createIntentChangeHandler() {
return (oldIntent, newIntent) => {
console.log("Intent transition:", oldIntent, "→", newIntent);
// Generate Image → Edit Image transition
if (oldIntent === "create-image-from-text") {
return createGenerateToEditTransition();
}
// Edit Image → Full Editor transition
if (oldIntent === "edit-image-v2") {
return createEditToFullEditorTransition();
}
return undefined;
};
}
/**
* Create the app configuration for Generate Image workflow
*/
export function createGenerateImageAppConfig() {
const intentChangeHandler = createIntentChangeHandler();
return createGenerateImageWorkflowConfig(baseCallbacks, intentChangeHandler);
}
/**
* Create the app configuration for Edit Image workflow
*/
export function createEditImageAppConfig() {
const intentChangeHandler = createIntentChangeHandler();
return createEditImageWorkflowConfig(baseCallbacks, intentChangeHandler);
}
workflows/generateToEdit.js
/**
* Generate Image to Edit Image Workflow
* Handles the transition from Generate Image workflow to Edit Image workflow
*/
import { generateImage, resetWorkflow } from "../utils/shared.js";
import { endEditImageExportConfig } from "../config/exportConfigs.js";
/**
* Publish callback for Generate Image workflow
* Updates the generated image and resets workflow state
*/
export async function handleGenerateImagePublish(intent, publishParams) {
console.log("Generate workflow - intent:", intent);
console.log("Generate workflow - publishParams:", publishParams);
// Update the left image (image1) with generated content
generateImage.src = publishParams.asset[0].data;
console.log("Updated generateImage (image1) with generated content");
// Reset workflow tracking
resetWorkflow();
}
/**
* Creates the workflow transition configuration for Generate Image → Edit Image
* This is returned by onIntentChange when transitioning from generate to edit
*/
export function createGenerateToEditTransition() {
return {
exportConfig: endEditImageExportConfig,
};
}
/**
* Complete workflow configuration for Generate Image
* Includes all features needed for the generate image workflow
*/
export function createGenerateImageWorkflowConfig(
baseCallbacks,
onIntentChangeHandler
) {
return {
appVersion: "2",
featureConfig: {
"community-wall": true,
"fast-mode": false,
"custom-models": false,
},
thumbnailOptions: ["rich-preview", "edit-dropdown"],
editDropdownOptions: [
{ option: "add-effects" },
{ option: "remove-background" },
{ option: "apply-adjustment" },
{ option: "insert-object" },
{ option: "remove-object" },
],
callbacks: {
...baseCallbacks,
onPublish: handleGenerateImagePublish,
onIntentChange: onIntentChangeHandler,
},
};
}
workflows/editToFullEditor.js
/**
* Edit Image to Full Editor Workflow
* Handles the transition from Edit Image workflow to Full Editor workflow
*/
import {
expressImage,
resetWorkflow,
updateImageAndCache,
baseCallbacks,
} from "../utils/shared.js";
import { endFullEditorExportConfig } from "../config/exportConfigs.js";
/**
* Publish callback for Edit Image workflow
* Updates the edited image and maintains blob cache
*/
export async function handleEditImagePublish(intent, publishParams) {
console.log("Edit workflow - intent:", intent);
console.log("Edit workflow - publishParams:", publishParams);
// Update the right image (image2) with edited content
await updateImageAndCache(expressImage, publishParams.asset[0].data, true);
console.log("Updated expressImage (image2) with edited content");
// Reset workflow tracking
resetWorkflow();
}
/**
* Publish callback for Full Editor workflow (nested)
* Handles the final result from the Full Editor
*/
export async function handleFullEditorPublish(intent, publishParams) {
console.log("Full editor workflow - intent:", intent);
console.log("Full editor workflow - publishParams:", publishParams);
// Save back to image2 (expressImage) - same logic as regular edit workflow
await updateImageAndCache(expressImage, publishParams.asset[0].data, true);
console.log("Updated expressImage (image2) with full editor content");
}
/**
* Creates the workflow transition configuration for Edit Image → Full Editor
* This is returned by onIntentChange when transitioning from edit to full editor
*/
export function createEditToFullEditorTransition() {
return {
appConfig: {
appVersion: "2",
callbacks: {
...baseCallbacks,
onPublish: handleFullEditorPublish,
},
},
exportConfig: endFullEditorExportConfig,
};
}
/**
* Complete workflow configuration for Edit Image
* Minimal configuration - just the essentials for editing
*/
export function createEditImageWorkflowConfig(
baseCallbacks,
onIntentChangeHandler
) {
return {
appVersion: "2",
callbacks: {
...baseCallbacks,
onPublish: handleEditImagePublish,
onIntentChange: onIntentChangeHandler,
},
};
}
Next steps
Congratulations! You've completed the advanced Workflow Tethering tutorial and built a sophisticated, modular architecture for complex Adobe Express integrations. You can now try to add new workflow transitions using the established patterns.
Need help?
Need help or have questions? Join our Community Forum to get help and connect with other developers working with advanced Adobe Express Embed SDK integrations.
Related resources
- Workflow Tethering guide: Comprehensive guide to understanding workflow tethering.
- Edit Image tutorial: Foundation for understanding basic image editing workflows.
- Generate Image tutorial: Essential knowledge for image generation workflows.
- API Reference: Complete SDK documentation
- Adobe Express Embed SDK Overview: High-level introduction
- Demo Application: Interactive demo showcasing SDK capabilities
- Sample Applications: Working code examples and tutorials
- Changelog: Latest updates and improvements