Edit in GitHubLog an issue

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:

  1. Generate Image → Edit Image: Users generate images and seamlessly transition to basic editing
  2. Edit Image → Full Editor: Users performing basic edits can access advanced editing capabilities

Workflow Tethering tutorial

Prerequisites

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.

Copied to your clipboard
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:

Copied to your clipboard
.
├── 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:

Copied to your clipboard
VITE_API_KEY="your-api-key-here!"

1.3 Install dependencies and run

Install the dependencies and start the development server:

Copied to your clipboard
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.

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 initialization
  • utils/shared.js: State management and common utilities
  • config/: Configuration objects and export button definitions
  • workflows/: Workflow-specific logic and transition handling

The main.js module is the orchestrator of the application.

Copied to your clipboard
// 👇 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:

Copied to your clipboard
// 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:

Copied to your clipboard
// 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.

Workflow Tethering - Generate Image

Copied to your clipboard
//... 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.

Copied to your clipboard
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);
}

The generateImageAppConfig object creation follows a dependency chain across these multiple files:

  • config/appConfigs.js: the createGenerateImageAppConfig() factory function orchestrates the configuration creation:

    • createIntentChangeHandler() is executed to create the onIntentChange() handler for workflow transitions. Internally, this function calls createGenerateToEditTransition() imported from workflows/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 baseCallbacks from utils/shared.js and the intentChangeHandler to createGenerateImageWorkflowConfig() from workflows/generateToEdit.js, which returns the complete configuration object for the Generate Image workflow.
  • utils/shared.js: Provides the foundational baseCallbacks object containing the onCancel and onError callbacks, shared across all workflows.

  • workflows/generateToEdit.js: The createGenerateImageWorkflowConfig() function builds the complete configuration object by:

    • Setting Generate Image-specific properties (appVersion, featureConfig, thumbnailOptions, editDropdownOptions).
    • Merging the baseCallbacks with workflow-specific callbacks like onPublish and onIntentChange.
    • The onIntentChange callback uses the intent change handler to detect transitions between workflows and pass the appropriate export configuration for the tethered workflow.

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

Workflow Tethering - Generate Image to Edit Image

Copied to your clipboard
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;
};
}

The user is now able to perform Image Editing routines, such as Removing Background, Adding Effects, and more.

Workflow Tethering - Edit Image editing

4.1.3 State synchronization

The publish callback ensures proper state management, and the image is updated with the generated content.

Workflow Tethering - Edit Image publishing

Copied to your clipboard
// ...
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.

Workflow Tethering - Edit Image

Copied to your clipboard
//... 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.

Copied to your clipboard
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);
}

The editImageAppConfig object creation follows a dependency chain across these multiple files:

  • config/appConfigs.js: the createEditImageAppConfig() factory function orchestrates the configuration creation:

    • createIntentChangeHandler() is executed to create the onIntentChange() handler for workflow transitions. Internally, this function calls createEditToFullEditorTransition() imported from workflows/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 baseCallbacks from utils/shared.js and the intentChangeHandler to createEditImageWorkflowConfig() from workflows/editToFullEditor.js, which returns the complete configuration object for the Edit Image workflow.
  • utils/shared.js: Provides the foundational baseCallbacks object containing the onCancel and onError callbacks, shared across all workflows.

  • workflows/editToFullEditor.js: The createEditImageWorkflowConfig() function builds the complete configuration object by:

    • Setting the Edit Image appVersion.
    • Merging the baseCallbacks with workflow-specific callbacks like onPublish and onIntentChange.
    • The onIntentChange callback uses the intent change handler to detect transitions between workflows and pass the appropriate export configuration for the tethered workflow.

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.

Workflow Tethering - Edit Image to Full Editor

Copied to your clipboard
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;
};
}

The user is now able to use the entire set of features available in Adobe Express, such as Adding Text or shapes.

Workflow Tethering - Full Editor

4.2.3 Dual state management

This workflow manages two different publish scenarios:

Copied to your clipboard
// 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.

Workflow Tethering - Dual publishing

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.js sets currentWorkflow = "generate"
    • SDK launches with generateImageAppConfig + startGenImageExportConfig
  • The user generates an image

    • handleGenerateImagePublish() updates generateImage.src
    • Workflow state resets
  • The user selects Edit image in the Generate Image experience:

    • onIntentChange() detects "create-image-from-text" as the old intent
    • createGenerateToEditTransition() returns endEditImageExportConfig
    • 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

5.2 Edit Image → Full Editor → Save

  • The user selects Edit Image (direct entry):

    • main.js sets currentWorkflow = "edit"
    • SDK launches with editImageAppConfig + startEditImageExportConfig
  • The user selects Do MoreAdd text (or other advanced option):

    • onIntentChange() detects "edit-image-v2" as the old intent
    • createEditToFullEditorTransition() returns endFullEditorExportConfig
    • SDK launches the Full Editor with different export buttons
  • The user performs advanced editing and clicks Save design

    • handleFullEditorPublish() updates expressImage.src
    • currentImageBlob cache updates for future edits
    • Workflow completes with advanced editing applied

Workflow Tethering - All workflows

Troubleshooting

Common issues

IssueSolution
Error: "Adobe Express is not available"
Check to have entered the correct API Key in the src/.env file as described here.

Complete working example

The complete implementation demonstrates all the concepts covered in this tutorial. The code is split in two blocks below for convenience.

Copied to your clipboard
<!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>
Copied to your clipboard
// 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" },
},
];

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.

  • Privacy
  • Terms of Use
  • Do not sell or share my personal information
  • AdChoices
Copyright © 2025 Adobe. All rights reserved.