<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Cloudinary</title>
    <description>The latest articles on DEV Community by Cloudinary (cloudinary).</description>
    <link>https://dev.to/cloudinary</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F7286%2F8677e80f-fe6d-40e5-a9d6-c72af0042cd8.png</url>
      <title>DEV Community: Cloudinary</title>
      <link>https://dev.to/cloudinary</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cloudinary"/>
    <language>en</language>
    <item>
      <title>From Cloud to Crowd: Welcome to Cloudinary Creators Cohort 2</title>
      <dc:creator>eugene musebe</dc:creator>
      <pubDate>Thu, 24 Sep 2026 15:34:32 +0000</pubDate>
      <link>https://dev.to/cloudinary/from-cloud-to-crowd-welcome-to-cloudinary-creators-cohort-2-4je9</link>
      <guid>https://dev.to/cloudinary/from-cloud-to-crowd-welcome-to-cloudinary-creators-cohort-2-4je9</guid>
      <description>&lt;p&gt;When we launched the &lt;a href="https://cloudinary.com/developers/community" rel="noopener noreferrer"&gt;Cloudinary Creators Community&lt;/a&gt; (CCC), we set out on a clear philanthropic mission: bridging the gap between foundational learning and real-world professional application for early-career, career-changing, and underrepresented developers.&lt;/p&gt;

&lt;p&gt;In close grassroots collaboration with our nonprofit partners including GirlScript Foundation, Developers in Vogue, Vets Who Code, Tampa Devs, and HackYourFuture we introduced a guiding vision coined by partner Jerome Hardaway: taking aspiring builders "From Cloud to Crowd." Through dedicated webinars, hands-on coaching, and the &lt;a href="https://training.cloudinary.com/pages/c2c" rel="noopener noreferrer"&gt;Cloudinary Academy&lt;/a&gt;, hundreds of talented learners stepped forward to build visual media literacy and take control of their technical journeys.&lt;/p&gt;

&lt;p&gt;As our first cohort grew, one lesson emerged above all others: passing a curriculum gives you the foundation, but building, writing, and sharing in public is what transforms a learner into an industry-recognized creator.&lt;/p&gt;

&lt;p&gt;Today, we are thrilled to officially unveil Cloudinary Creators: Cohort 2.&lt;/p&gt;

&lt;p&gt;Cohort 2 honors our nonprofit roots while completely reimagining the creator journey. We are replacing passive course completion and complex annual point trackers with a transparent, merit-based pathway centered on real execution: Learn, Apply, Build or Write, Showcase, and Contribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cohort 2? The Shift from Learner to Creator
&lt;/h2&gt;

&lt;p&gt;In our initial phase, passing the Academy final assessment was treated as the finish line to enter the community. In Cohort 2, course completion is not the finish line. It is where your journey actually begins.&lt;/p&gt;

&lt;p&gt;We designed Cohort 2 around four core principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Empowering Diverse Builders:&lt;/strong&gt; Deepening our commitment to underrepresented talent and nonprofit cohorts by offering tangible portfolio-building opportunities rather than theoretical exercises.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Earning Your Stripes in Public:&lt;/strong&gt; Full creator status, official credential badges, and swag are unlocked when you demonstrate your work in public.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sustainable, High-Value Contributions:&lt;/strong&gt; We replaced confusing annual point systems with straightforward, quarterly participation goals that fit busy lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured Cohort Capacity:&lt;/strong&gt; Intentional intake caps ensure every single candidate receives direct access to DevRel mentorship, feedback, and career advocacy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The New 4-Stage Creator Journey
&lt;/h2&gt;

&lt;p&gt;The hallmark of Cohort 2 is a clear progression that guides you from a curious student to an established builder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1: Learn + Apply&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
 ↓&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Stage 2: Creator Candidate (Provisional Acceptance)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
 ↓&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Stage 3: Build or Write + Showcase (30-Day Milestone)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
 ↓&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Stage 4: Official Cloudinary Creator (Full Benefits &amp;amp; Quarterly Engagement)&lt;/strong&gt;  &lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: Learn + Apply
&lt;/h3&gt;

&lt;p&gt;Everything begins with media mastery. Learners from our partner communities and independent applicants complete the free Cloud to Crowd curriculum on Cloudinary Academy and pass the comprehensive assessment with a score of 80% or higher.&lt;/p&gt;

&lt;p&gt;Passing the exam earns an official certification and makes you eligible to apply for an upcoming cohort. However, course completion alone no longer grants automatic membership. Applications are reviewed based on cohort capacity, goals, and alignment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: Become a Creator Candidate
&lt;/h3&gt;

&lt;p&gt;Once accepted, you enter the community under our provisional status: Creator Candidate.&lt;/p&gt;

&lt;p&gt;As a Candidate, you gain immediate access to our Discord community space to introduce yourself on the brag board, meet fellow builders, and access team office hours. Most importantly, your 30-day countdown begins to complete your official milestone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: The 30-Day Creator Milestone (Build or Write)
&lt;/h3&gt;

&lt;p&gt;To move from Candidate to full Creator, you must demonstrate how you apply Cloudinary to solve a real-world problem within 30 days. Because developers communicate their expertise in different ways, you can choose between two tracks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Option 1: Build &amp;amp; Share&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Build a functional project solving a practical media challenge using Cloudinary. Share what you built, the media challenge you tackled, how Cloudinary solved it, key lessons learned, and links to your public repository or live demo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Option 2: Write &amp;amp; Share&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Prefer technical writing? Research, write, and publish an in-depth article on your personal blog, DEV Community (dev.to), or another public platform explaining how you solved a real-world media problem with Cloudinary.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stage 4: Official Cloudinary Creator Status
&lt;/h3&gt;

&lt;p&gt;Once your milestone is reviewed, confirmed, and shared with the community, you officially graduate to a Cloudinary Creator!&lt;/p&gt;

&lt;h2&gt;
  
  
  Unlocking Full Creator Benefits
&lt;/h2&gt;

&lt;p&gt;In Cohort 2, premium perks are directly tied to completing your milestone and earning full Creator status:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudinary Creators Plan Upgrade:&lt;/strong&gt; Eligible creators receive a complimentary account upgrade with increased limits and features to host personal portfolio projects and experimental applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exclusive Creator Swag Pack:&lt;/strong&gt; Custom apparel, stickers, and creator gear delivered directly to your doorstep once your milestone is verified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Official Badges &amp;amp; Industry Credentials:&lt;/strong&gt; Digital badges commemorating your educational achievements, milestone delivery, and community contributions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevRel Mentorship &amp;amp; Technical Office Hours:&lt;/strong&gt; Direct access to the Cloudinary Developer Relations team for architectural reviews, troubleshooting, and career advice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Community Network:&lt;/strong&gt; Direct collaboration with peers and mentors from international partner organizations including GirlScript Foundation, Developers in Vogue, Vets Who Code, and HackYourFuture.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Staying Active: The New Quarterly Model
&lt;/h2&gt;

&lt;p&gt;We have eliminated confusing scoreboards and complicated point tallies. To keep your Creator status active in Cohort 2, members simply participate in at least 2 creator activities per quarter.&lt;/p&gt;

&lt;p&gt;You choose the activities that best match your schedule and strengths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Participate in a Mini-Hack:&lt;/strong&gt; Join our community build challenges, either solo or with a partner. (1 qualifying mini-hack = 1 activity)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a Community Blog Post:&lt;/strong&gt; Publish a technical guide, tutorial, or project breakdown. (1 qualifying post = 1 activity)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Help Organize a Local Community Event:&lt;/strong&gt; Support local developer meetups through workshop facilitation, technical talks, meetup planning, or outreach. (1 qualifying event contribution = 1 activity)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you choose two mini-hacks, one article and one event, or two community blog posts, staying active is about meaningful participation that enriches the wider developer ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Face of CCC: Celebrating Our Builders
&lt;/h2&gt;

&lt;p&gt;We want to highlight our creators across the visual web. As part of the Cohort 2 onboarding flow, Creator Candidates and official Creators are invited to upload their professional headshots to Cloudinary and submit their Cloudinary asset URLs. Outstanding builders and milestone projects will be regularly spotlighted across Cloudinary social channels, newsletters, and community showcases. Check out some of our Creators at &lt;a href="https://community.cloudinary.com" rel="noopener noreferrer"&gt;https://community.cloudinary.com&lt;/a&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Get Started
&lt;/h2&gt;

&lt;p&gt;Ready to start your journey from Cloud to Crowd?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://training.cloudinary.com/pages/c2c" rel="noopener noreferrer"&gt;Take the Course&lt;/a&gt;:&lt;/strong&gt; Complete the Cloud to Crowd Curriculum on Cloudinary Academy and pass your assessment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://forms.monday.com/forms/7d800e93c41d672ed165eed8eb52572c?r=use1" rel="noopener noreferrer"&gt;Submit Your Interest&lt;/a&gt;:&lt;/strong&gt; Fill out the CCC Interest Form with your Academy Certificate details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build or Write:&lt;/strong&gt; Once accepted as a Candidate, complete your 30-day milestone and show the community what you can build.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Welcome to Cloudinary Creators: Cohort 2. Let us learn, build, share, and grow together.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Cloudinary ❤️ developers&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👉 &lt;strong&gt;&lt;a href="https://link.cloudinary.com/us6My" rel="noopener noreferrer"&gt;Create your free account&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;**Meet us on&lt;a href="https://discord.com/invite/D8ddQj6KnH" rel="noopener noreferrer"&gt;Discord!&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloudinary</category>
      <category>webdev</category>
      <category>community</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I Built an Agent That Does a Week of Work in One Command</title>
      <dc:creator>Sharon Yelenik</dc:creator>
      <pubDate>Thu, 24 Sep 2026 11:43:32 +0000</pubDate>
      <link>https://dev.to/cloudinary/i-built-an-agent-that-does-a-week-of-work-in-one-command-57i7</link>
      <guid>https://dev.to/cloudinary/i-built-an-agent-that-does-a-week-of-work-in-one-command-57i7</guid>
      <description>&lt;p&gt;Agents are here to make work more efficient, but not to take over taste and judgment. Finding the point where an agent can save real time without quietly taking the wheel is harder than it sounds.&lt;/p&gt;

&lt;p&gt;The Product Launch Agent below is my attempt at that sweet spot. You can find it here, on &lt;a href="https://github.com/cloudinary-devs/product-launch-agent" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;It leans on trusted, deterministic tools and human-curated inputs to do a week of work in one command, without taking the wheel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build your own product launch agent
&lt;/h3&gt;

&lt;p&gt;Clone the &lt;a href="https://github.com/cloudinary-devs/product-launch-agent" rel="noopener noreferrer"&gt;Product Launch Agent repository&lt;/a&gt;, connect a Cloudinary product environment, and generate a launch kit from your own approved assets.&lt;/p&gt;

&lt;p&gt;In about 15 minutes, you can upload and tag a launch image, let the agent find it, and generate platform-specific image variations and launch content.&lt;/p&gt;

&lt;h4&gt;
  
  
  The build prompt
&lt;/h4&gt;

&lt;p&gt;I used this prompt to describe the app I wanted to build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Build a Node.js CLI using the Anthropic SDK and Cloudinary.

The CLI should accept a launch brief describing the product, its key benefits, target audience, campaign strategy, and messaging.

Use an agent to produce:

- Release notes, product documentation, and a blog post
- Social posts for four platforms, each with its own correctly cropped hero image
- An outreach plan and draft direct messages for a list of influencers

The agent should use approved launch assets uploaded to Cloudinary and tagged with a launch identifier. Find those assets, create platform-specific crops and an Open Graph image with a text overlay, and pass the resulting URLs to the content-generation tools.

Never invent or substitute an image when no suitable approved asset is available. Outreach messages must be drafts only and must never be sent automatically.

Save the complete launch kit as a polished HTML report in:

output/&amp;lt;launch-name&amp;gt;/report.html

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This prompt is separate from the system prompt that guides the agent at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Product Launch Agent
&lt;/h2&gt;

&lt;p&gt;A human team member tags and approves the images, selects the campaign strategy, and writes the messaging brief. The agent finds and formats what was approved. It does not decide what looks good or what the brand should say.&lt;/p&gt;

&lt;p&gt;One command produces a blog post, release notes, a social campaign across four platforms, an outreach plan, and drafted DMs to a list of influencers, ready for review. Done manually, this could take a full week:&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;h3&gt;
  
  
  The Cloudinary workflow
&lt;/h3&gt;

&lt;p&gt;Before running the agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload approved launch assets to Cloudinary.&lt;/li&gt;
&lt;li&gt;Tag the assets with a launch identifier, such as &lt;code&gt;vesper-no-1&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then the agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Searches for those assets by tag.&lt;/li&gt;
&lt;li&gt;Generates platform-specific crops with &lt;code&gt;crop: 'fill'&lt;/code&gt; and &lt;code&gt;gravity: 'auto'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Passes the resulting Cloudinary URLs to the content-generation tools.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cloudinary handles asset storage, search, transformation, optimization, and delivery. The agent orchestrates the automated operations and assembles the launch kit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before you run the agent
&lt;/h3&gt;

&lt;p&gt;Make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://cloudinary.com/users/register_free" rel="noopener noreferrer"&gt;Cloudinary product environment&lt;/a&gt; and its &lt;a href="https://link.cloudinary.com/us4C8" rel="noopener noreferrer"&gt;API credentials&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;At least one uploaded image with a launch tag, such as &lt;code&gt;vesper-no-1&lt;/code&gt;. See &lt;a href="https://link.cloudinary.com/us4C9" rel="noopener noreferrer"&gt;Manage asset tags&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;An Anthropic API key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To try the agent quickly, use one of the sample images included in a new account. In the Media Library, select an asset and choose &lt;strong&gt;Metadata &amp;gt; Edit Tags&lt;/strong&gt; to add the launch tag.&lt;/p&gt;

&lt;p&gt;You’re ready when the agent can find your tagged asset and return at least one transformed image URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Similar agents you can create
&lt;/h2&gt;

&lt;p&gt;The same pattern can support a real estate agent that formats listings, a support agent that summarizes logs, or an events agent that creates a highlight reel and recap gallery.&lt;/p&gt;

&lt;p&gt;Here’s exactly how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture, in one breath
&lt;/h2&gt;

&lt;p&gt;The repository separates the launch brief, agent loop, tools, and report generation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmeelkuwq3ah479tm4ui3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmeelkuwq3ah479tm4ui3.png" alt="Product Launch Agent architecture" width="799" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;index.js&lt;/code&gt; gets a launch brief from the interactive wizard or a pre-created file. It passes that brief to &lt;code&gt;runProductLaunchAgent()&lt;/code&gt; in &lt;code&gt;agent.js&lt;/code&gt;, along with Claude’s system prompt and the available tools.&lt;/p&gt;

&lt;p&gt;The agent loop decides which tools to call and in what order. Content tools generate the launch materials, Cloudinary tools find and transform the approved assets, and &lt;code&gt;report.js&lt;/code&gt; assembles the results into an HTML report.&lt;/p&gt;

&lt;p&gt;The main files are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
src/
├── index.js                # entry point
├── wizard.js               # builds the launch brief
├── agent.js                # runs the agent loop
├── report.js               # creates the HTML report
└── tools/
    ├── tool-definitions.js # schemas and routing
    ├── content-tools.js    # content generation
    └── cloudinary-tools.js # Cloudinary operations

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tools give the agent capabilities
&lt;/h2&gt;

&lt;p&gt;Claude can't access Cloudinary or generate content directly. The application gives it tools. Each tool has a name, a description, and a structured input schema that tells Claude what the tool does and what information it requires.&lt;/p&gt;

&lt;p&gt;For example, this tool finds approved assets for a launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{
  name: 'find_launch_assets',
  description:
    'Find assets already uploaded and tagged for this launch. Call this first.',
  input_schema: {
    type: 'object',
    properties: {
      tag: { type: 'string' },
      resourceType: { type: 'string', enum: ['image', 'video'] },
    },
    required: ['tag'],
  },
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The description is part of the agent’s instructions. “Call this first” helps Claude resolve the available imagery before requesting content that depends on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating platform-specific crops
&lt;/h3&gt;

&lt;p&gt;The user uploads and tags the approved launch assets. The agent searches for them, then requests the required transformations from Cloudinary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp53oqlntrlw68wq08ogs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp53oqlntrlw68wq08ogs.png" alt="Cloudinary flow" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;generateSocialCrops()&lt;/code&gt; applies platform-specific crop settings, so every platform gets a predictable output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const CROP_PRESETS = {
  instagram_square: { width: 1080, height: 1080, crop: 'fill', gravity: 'auto' },
  x_post: { width: 1600, height: 900, crop: 'fill', gravity: 'auto' },
  linkedin_post: { width: 1200, height: 627, crop: 'fill', gravity: 'auto' },
};

const url = cloudinary.url(publicId, {
  transformation: [
    {
      ...CROP_PRESETS[platform],
      fetch_format: 'auto',
      quality: 'auto',
    },
  ],
  secure: true,
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gravity: 'auto'&lt;/code&gt; helps Cloudinary keep the subject visible when the same source image is cropped into different shapes. &lt;code&gt;fetch_format: 'auto'&lt;/code&gt; delivers an efficient format for the requesting browser, while &lt;code&gt;quality: 'auto'&lt;/code&gt; balances visual quality and file size. The resulting optimized URLs are returned to Claude, which uses the appropriate image in each piece of launch content.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent loop
&lt;/h2&gt;

&lt;p&gt;Claude decides which tool to call next. After a tool runs, its result is added to the conversation history, so Claude can use that result when making its next decision.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const messages = [{ role: 'user', content: brief }];

for (let turn = 0; turn &amp;lt; maxTurns; turn++) {
  const response = await anthropic.messages.create({
    model: MODEL,
    system: SYSTEM_PROMPT,
    tools: TOOLS,
    messages,
  });

  messages.push({ role: 'assistant', content: response.content });

  const toolUseBlocks = response.content.filter(
    (block) =&amp;gt; block.type === 'tool_use',
  );

  if (toolUseBlocks.length === 0) break;

  const toolResults = [];
  for (const block of toolUseBlocks) {
    const output = await executeTool(block.name, block.input);
    toolResults.push({
      type: 'tool_result',
      tool_use_id: block.id,
      content: JSON.stringify(output),
    });
  }

  messages.push({ role: 'user', content: toolResults });
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two &lt;code&gt;messages.push()&lt;/code&gt; calls serve different purposes. The first records Claude’s response. The second records the results of the tools Claude requested. Together, they give Claude the context it needs to choose the next step.&lt;/p&gt;

&lt;p&gt;Claude can request several tools in one response. The application runs each one and matches every result to the original request with &lt;code&gt;tool_use_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The loop stops when Claude returns a response without any &lt;code&gt;tool_use&lt;/code&gt; blocks. That response is the completion signal: Claude has decided it has everything it needs for the launch kit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example run
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Turn&lt;/th&gt;
      &lt;th&gt;Claude asks for&lt;/th&gt;
      &lt;th&gt;What comes back&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;&lt;code&gt;find_launch_assets&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;The launch images&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;
&lt;code&gt;generate_social_crops&lt;/code&gt;, &lt;code&gt;generate_og_image&lt;/code&gt;
&lt;/td&gt;
      &lt;td&gt;Crop URLs and an Open Graph image URL&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;&lt;code&gt;generate_blog_draft&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;The generated blog draft&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;Plain text; no tools&lt;/td&gt;
      &lt;td&gt;Final summary; the loop stops&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The run that produced the report above made 12 tool calls within the 14-turn limit. The limit prevents the agent from continuing indefinitely if it gets stuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code-Level Safeguards
&lt;/h2&gt;

&lt;p&gt;Prompts guide the agent, but important rules are also enforced in code. For example, content that embeds media can't be generated until the application has attempted to find or upload a launch asset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const ASSET_STEP_TOOLS = new Set([
  'find_launch_assets',
  'upload_launch_image',
]);
const MEDIA_DEPENDENT_TOOLS = new Set([
  'generate_blog_draft',
  'generate_social_posts',
]);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no approved image is available, the agent stops rather than inventing one. As the system prompt puts it: “A fabricated image is worse than no image.” The code-level check makes that rule enforceable even if the model misinterprets the prompt.&lt;/p&gt;

&lt;p&gt;This is the balance the application is designed to strike: Claude handles flexible orchestration, while the tools and application code control what the agent is allowed to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take it beyond the demo: Elevating taste over execution
&lt;/h2&gt;

&lt;p&gt;If you find the Product Launch Agent helpful, &lt;a href="http://github.com/cloudinary-devs/product-launch-agent" rel="noopener noreferrer"&gt;clone the repository&lt;/a&gt; and connect it to your own Cloudinary product environment. Start with one tagged image and one launch brief, then expand the workflow as you learn what your team wants to automate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try extending the agent
&lt;/h3&gt;

&lt;p&gt;You can extend the same workflow to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search by structured metadata instead of tags.&lt;/li&gt;
&lt;li&gt;Generate video variations as well as images.&lt;/li&gt;
&lt;li&gt;Add automatic format and quality optimization.&lt;/li&gt;
&lt;li&gt;Store generated launch assets in a dedicated folder.&lt;/li&gt;
&lt;li&gt;Use webhooks to notify the team when a launch kit is ready.&lt;/li&gt;
&lt;li&gt;Add an approval step before assets are published.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These extensions turn the demo into a reusable production workflow. Cloudinary handles the repeatable media operations, while the agent orchestrates the work and leaves creative decisions to your team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built your own launch agent?
&lt;/h3&gt;

&lt;p&gt;Share what you generated or &lt;a href="http://github.com/cloudinary-devs/product-launch-agent/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; in the repository with your results. Tell us which Cloudinary operations you used and what you would like the agent to do next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search &amp;amp; discovery (keywords):&lt;/strong&gt; AI agents Cloudinary; Claude tool use; Anthropic API agents; Cloudinary asset search; Cloudinary image transformations; dynamic image URLs; &lt;code&gt;gravity: auto&lt;/code&gt;; &lt;code&gt;crop: fill&lt;/code&gt;; agent orchestration; deterministic tools; human-in-the-loop AI; automated content workflows; product launch automation; JavaScript SDK; media asset management.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Cloudinary ❤️ developers&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready to build an agent that turns approved media into a complete launch kit? Start using Cloudinary for free and automate your image search, transformations, and delivery workflows.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👉 &lt;strong&gt;&lt;a href="https://link.cloudinary.com/us4C7" rel="noopener noreferrer"&gt;Create your free account&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>productlaunch</category>
      <category>cli</category>
    </item>
    <item>
      <title>What Does WebMCP Really Unlock?</title>
      <dc:creator>Ebony Louis</dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:57:51 +0000</pubDate>
      <link>https://dev.to/cloudinary/what-does-webmcp-really-unlock-dj4</link>
      <guid>https://dev.to/cloudinary/what-does-webmcp-really-unlock-dj4</guid>
      <description>&lt;p&gt;I first started playing around with &lt;a href="https://developer.chrome.com/docs/ai/agents" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt; back in March.&lt;/p&gt;

&lt;p&gt;The idea immediately made sense to me: instead of forcing an AI agent to inspect a webpage, figure out what everything means, and interact with an interface built entirely for us, the website can expose structured tools that tell the agent what it can actually do.&lt;/p&gt;

&lt;p&gt;Fast forward a few months and WebMCP is suddenly everywhere. I've seen demos of agents filling out forms, navigating pages, adding things to carts, and interacting with websites. They're cool demos, but I think there's a bigger idea hiding underneath them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebMCP isn't just a better way for an agent to operate a webpage. It gives the website a way to participate in the agent experience.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That means the agent can understand more than the actions available to it. The webpage can also expose the context of the experience we're already in: which product I've selected, which image I'm looking at, which part of the application I'm in, or whether there's a change waiting for my approval.&lt;/p&gt;

&lt;p&gt;I wanted to build something that made that relationship visible. So I built a small merchant experience using &lt;strong&gt;&lt;a href="https://developer.chrome.com/docs/ai/agents" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt;, &lt;a href="https://cloudinary.com/" rel="noopener noreferrer"&gt;Cloudinary&lt;/a&gt;, and &lt;a href="https://block.github.io/goose/" rel="noopener noreferrer"&gt;Goose&lt;/a&gt;&lt;/strong&gt; where I can work alongside my AI agent on the same product workflow.&lt;/p&gt;

&lt;p&gt;The goal wasn't to see whether an agent could edit an image. We already know it can. I wanted to show what happens when &lt;strong&gt;the agent isn't just operating the webpage, but actually understands the experience we're already in.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/eLTQiL68swI" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Watch the full workflow above: selecting a product, asking Goose what I'm looking at, searching the Media Library, previewing Cloudinary's edit, and applying it to the storefront.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The merchant workspace is intentionally simple. There are &lt;strong&gt;three products&lt;/strong&gt;, &lt;strong&gt;a Media Library connected to my Cloudinary account&lt;/strong&gt;, and a &lt;strong&gt;customer facing storefront&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The webpage exposes WebMCP tools for the things an agent might need to do inside that experience: list products, navigate between sections, understand which product I currently have selected, search my Media Library, select an asset, create an image preview, apply or discard that preview, and publish a new product image.&lt;/p&gt;

&lt;p&gt;I started by asking Goose:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Open localhost:8002. Using the WebMCP tools exposed by this page, tell me what products are available.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Goose found all three products.&lt;/p&gt;

&lt;p&gt;Then I clicked the &lt;em&gt;Midnight Slip Dress&lt;/em&gt; myself. I didn't tell Goose what I clicked or give it a product ID. I simply asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What product am I looking at?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It knew.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa3wp0l50lyr0sf3blbuj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa3wp0l50lyr0sf3blbuj.png" alt="dress" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That sounds like a small interaction, but it's one of the easiest ways to see what WebMCP changes. The agent and I are working against the same webpage and the same state.&lt;/p&gt;

&lt;p&gt;From there, I asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Take me to the Media Library and find me some other black dress images.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The page navigated to the Media Library and searched my Cloudinary account for matching assets.&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the best one. Make the dress blue, put the model in a bright luxury fashion studio, and prepare it for the product grid. Show me a preview, but don't apply it yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cloudinary handled the media work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cloudinary.com/documentation/generative_recolor" rel="noopener noreferrer"&gt;Generative Recolor&lt;/a&gt; changed the dress from black to blue&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloudinary.com/documentation/generative_background_replace" rel="noopener noreferrer"&gt;Generative Background Replace&lt;/a&gt; created the new environment around the model&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloudinary.com/documentation/transformation_reference" rel="noopener noreferrer"&gt;Cloudinary's transformation capabilities&lt;/a&gt; prepared the resulting image for the product grid&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the agent didn't immediately update the product. The page returned me to the product editor with an &lt;strong&gt;AI PREVIEW — NOT APPLIED&lt;/strong&gt; state so I could review the result first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh3cm07mghhrjswhxcqag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh3cm07mghhrjswhxcqag.png" alt="cloudinary" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once I was happy with it, I told Goose:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Looks perfect. Apply it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The blue dress was now published on my storefront.&lt;/p&gt;

&lt;p&gt;The entire workflow looked something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhlfta1alkenlvm8pqxu1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhlfta1alkenlvm8pqxu1.png" alt="webmcp flow" width="800" height="1143"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually Happening?
&lt;/h2&gt;

&lt;p&gt;The interaction that starts this workflow is the one I find most useful for understanding WebMCP: I select a product myself and the agent knows what I'm referring to.&lt;/p&gt;

&lt;p&gt;Normally, if I wanted an agent to modify a particular product, I'd need to give it enough information to figure out which product I meant. I could tell it the name, give it an ID, or, if it has browser access, let it inspect the page and try to determine what I'm interacting with.&lt;/p&gt;

&lt;p&gt;WebMCP gives us another option. &lt;strong&gt;The page itself can expose that state.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Chrome's &lt;a href="https://developer.chrome.com/docs/ai/webmcp/imperative-api" rel="noopener noreferrer"&gt;WebMCP Imperative API&lt;/a&gt; allows a site to register tools using &lt;code&gt;document.modelContext&lt;/code&gt;. Each tool can define a name, description, input schema, and JavaScript implementation for an action the site knows how to perform.&lt;/p&gt;

&lt;p&gt;In my demo, one of those tools is &lt;code&gt;get_page_context&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_page_context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Get current merchant page context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Returns the current section of this merchant webpage, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;the product the human currently has selected, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;the selected Cloudinary media asset, and whether &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;an unapplied image preview exists.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;additionalProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;currentView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;selectedProduct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getSelectedProduct&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;selectedMedia&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hasPreview&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I ask, "What product am I looking at?" the agent doesn't have to infer what I mean from the interface. The website already knows which product I've selected and can expose that information through &lt;code&gt;get_page_context&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same idea applies to actions. The page can expose a &lt;code&gt;search_media_library&lt;/code&gt; tool because it knows how media search works inside this application. It can expose &lt;code&gt;edit_current_product_image&lt;/code&gt; because it knows how product image editing works, and &lt;code&gt;apply_current_image_changes&lt;/code&gt; because it knows how an approved preview becomes the current product image.&lt;/p&gt;

&lt;p&gt;At this point, it's also important to separate what WebMCP is doing from what Cloudinary is doing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebMCP provides the context and actions&lt;/strong&gt; available within the merchant experience. &lt;strong&gt;Cloudinary provides the media capabilities&lt;/strong&gt; behind some of those actions. When the agent searches for another black dress, the search runs against my Cloudinary Media Library. When I ask it to make the dress blue and move the model into a fashion studio, Cloudinary handles the &lt;a href="https://cloudinary.com/documentation/generative_recolor" rel="noopener noreferrer"&gt;Generative Recolor&lt;/a&gt; and &lt;a href="https://cloudinary.com/documentation/generative_background_replace" rel="noopener noreferrer"&gt;Generative Background Replace&lt;/a&gt; transformations.&lt;/p&gt;

&lt;p&gt;Goose sits between my intent and those capabilities. Instead of giving it a product ID, telling it which functions to call, constructing the Cloudinary transformations myself, and then telling it where to navigate next, I can describe the outcome I want and let the agent use the capabilities the page has exposed.&lt;/p&gt;

&lt;p&gt;Technically, I could give an agent direct access to Cloudinary through an MCP server or API and ask it to transform an image. That would still be useful, but it wouldn't demonstrate what I wanted to show here. WebMCP connects those capabilities to the webpage I'm already using, including its current state and the actions that make sense within that experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting Your Agent to WebMCP
&lt;/h2&gt;

&lt;p&gt;Exposing WebMCP tools from the webpage is only half of the setup. Your agent also needs a way to discover and execute them.&lt;/p&gt;

&lt;p&gt;For this demo, I used &lt;strong&gt;&lt;a href="https://block.github.io/goose/" rel="noopener noreferrer"&gt;Goose&lt;/a&gt;&lt;/strong&gt; as my agent and &lt;strong&gt;&lt;a href="https://github.com/ChromeDevTools/chrome-devtools-mcp" rel="noopener noreferrer"&gt;Chrome DevTools MCP&lt;/a&gt;&lt;/strong&gt; to connect Goose to Chrome.&lt;/p&gt;

&lt;p&gt;This part came with a few gotchas that are worth covering because simply connecting Chrome DevTools MCP to your agent doesn't automatically mean you're ready to use WebMCP.&lt;/p&gt;

&lt;p&gt;When I built this demo, the configuration that got everything working was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; chrome-devtools-mcp@latest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--isolated&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--categoryExperimentalWebmcp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--chromeArg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--enable-features&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;WebMCPTesting,DevToolsWebMCPSupport
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few pieces here worth understanding rather than just copying the command.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--categoryExperimentalWebmcp=true&lt;/code&gt; enables Chrome DevTools MCP's experimental WebMCP tooling. This is what gives the agent access to capabilities for discovering and executing the WebMCP tools registered by the active page.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--chromeArg&lt;/code&gt; option passes feature flags to the Chrome instance being controlled by Chrome DevTools MCP. For the Chrome version I used while building this demo, I needed &lt;code&gt;WebMCPTesting&lt;/code&gt; and &lt;code&gt;DevToolsWebMCPSupport&lt;/code&gt; enabled for the browser side WebMCP integration.&lt;/p&gt;

&lt;p&gt;Finally, &lt;code&gt;--isolated&lt;/code&gt; isn't a WebMCP requirement. I added it after running into Chrome profile locking issues while repeatedly starting and stopping the demo. It launches Chrome with a temporary user data directory, which made repeated testing smoother.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; WebMCP is experimental and this setup is changing quickly. The flags above are the configuration I used for this demo. Check the current &lt;a href="https://developer.chrome.com/docs/ai/agents" rel="noopener noreferrer"&gt;WebMCP documentation&lt;/a&gt; and &lt;a href="https://github.com/ChromeDevTools/chrome-devtools-mcp" rel="noopener noreferrer"&gt;Chrome DevTools MCP documentation&lt;/a&gt; for the version you're using before setting up your own project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once the connection was working, Goose could discover the ten tools exposed by my merchant page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    &lt;code&gt;get_page_context&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;    &lt;code&gt;navigate_merchant_page&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;    &lt;code&gt;list_products_on_page&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;    &lt;code&gt;select_product&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;    &lt;code&gt;search_media_library&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;    &lt;code&gt;select_media_asset&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;    &lt;code&gt;edit_current_product_image&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;    &lt;code&gt;apply_current_image_changes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;    &lt;code&gt;discard_current_image_changes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;    &lt;code&gt;set_current_product_status&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I intentionally exposed smaller capabilities rather than creating one tool that performed the entire workflow. That gives the agent room to compose actions based on my intent while leaving places for me to participate, like manually selecting a product or approving a generated preview.&lt;/p&gt;

&lt;p&gt;One thing that tripped me up while testing: your agent successfully completing a task on your webpage doesn't necessarily mean it's using WebMCP.&lt;/p&gt;

&lt;p&gt;In one of my first tests, I asked Goose what products were available and it correctly returned all three. But when I looked at the tool calls, Goose had taken a snapshot of the page and read the products from the interface instead of using the WebMCP tools I'd exposed.&lt;/p&gt;

&lt;p&gt;Once everything was configured correctly, I could see Goose discovering and executing the tools registered by the webpage. So if your agent also has browser automation capabilities, don't test WebMCP based only on whether it gets the right result. Check the actual tool calls and make sure it's using the tools your page exposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What WebMCP Really Unlocks
&lt;/h2&gt;

&lt;p&gt;Agents could already interact with websites using screenshots, DOM inspection, browser automation, and other approaches. WebMCP doesn't replace any of that. It adds another interaction model: instead of requiring an agent to infer everything about an experience from an interface that was built for us, the website can explicitly describe what it knows and what it can do.&lt;/p&gt;

&lt;p&gt;I don't disappear from that workflow either. I select the product myself, let the agent handle the media work, review the result inside the same application, approve it, and let the agent continue.&lt;/p&gt;

&lt;p&gt;Not just a website an agent can operate. A shared environment where I can work alongside the agent, both of us acting on the same page state.&lt;/p&gt;

&lt;p&gt;If you want to experiment with this yourself, start with the &lt;a href="https://developer.chrome.com/docs/ai/agents" rel="noopener noreferrer"&gt;WebMCP documentation&lt;/a&gt;, explore &lt;a href="https://cloudinary.com/documentation/generative_ai_transformations" rel="noopener noreferrer"&gt;Cloudinary's Generative AI transformations&lt;/a&gt;, and check out &lt;a href="https://github.com/ChromeDevTools/chrome-devtools-mcp" rel="noopener noreferrer"&gt;Chrome DevTools MCP&lt;/a&gt; to connect an agent to Chrome.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Cloudinary ❤️ developers&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👉 &lt;strong&gt;&lt;a href="https://link.cloudinary.com/usLzb" rel="noopener noreferrer"&gt;Create your free account&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>agents</category>
      <category>webmcp</category>
    </item>
    <item>
      <title>Automate Cloudinary Image-to-Video Workflows with n8n</title>
      <dc:creator>Eitan Peer</dc:creator>
      <pubDate>Wed, 26 Aug 2026 18:44:16 +0000</pubDate>
      <link>https://dev.to/cloudinary/automate-cloudinary-image-to-video-workflows-with-n8n-36jb</link>
      <guid>https://dev.to/cloudinary/automate-cloudinary-image-to-video-workflows-with-n8n-36jb</guid>
      <description>&lt;p&gt;This n8n workflow automates Cloudinary image-to-video generation from a tagged source image and a prompt. It gives media teams a repeatable no-code process that creates, tracks, and stores the finished video beside its source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The upload:&lt;/strong&gt; a 728×546 WebP, 26 KB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8bgnay8wgohrfxr55tc0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8bgnay8wgohrfxr55tc0.webp" alt="Street photograph of a man walking with a woman while turning to look back at another woman passing by; she reacts with a disapproving expression."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sentence:&lt;/strong&gt; &lt;code&gt;Slow cinematic push-in with subtle parallax and slight lateral drift.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; the subjects and framing are preserved; the camera does the work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk288prqyrvud8h3h0uks.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk288prqyrvud8h3h0uks.webp" alt="Animated preview of the generated video: the still photograph moves with a slow push-in and slight lateral drift."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://player.cloudinary.com/embed/?cloud_name=eitanpeer&amp;amp;public_id=image-to-video%2Fi2v_9f3d639b5fb74adbb838a2780efbffc6&amp;amp;player[loop]=true&amp;amp;player[muted]=true&amp;amp;player[fluid]=true" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Watch the real MP4 in the Cloudinary Video Player&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Eleven n8n nodes, with no code. The generation call is one HTTP request. The workflow defines which uploads qualify, where the prompt lives, how the asynchronous job is tracked, and how to find the video later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Trigger Cloudinary image-to-video generation from an upload.&lt;/li&gt;
&lt;li&gt;Control eligibility with tags and metadata.&lt;/li&gt;
&lt;li&gt;Poll an asynchronous generation job safely.&lt;/li&gt;
&lt;li&gt;Tag the source image and return an embeddable video URL.&lt;/li&gt;
&lt;li&gt;Import and adapt the n8n workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What decides that an upload becomes a video
&lt;/h2&gt;

&lt;p&gt;Every rule below is a choice this example made, not a requirement of the API. All three live in a single &lt;strong&gt;Eligible?&lt;/strong&gt; IF node, so swap them for whatever your own pipeline keys off: a folder path, an upload preset, a structured metadata field, a public ID prefix, or the uploading user. What matters is that the check happens once, in one place, and that everything failing it stops there.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resource_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;is &lt;code&gt;image&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Video uploads land in the same notification stream and would otherwise loop.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;contains &lt;code&gt;video_gen&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;This is the opt-in.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;context.custom.prompt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;is not empty&lt;/td&gt;
&lt;td&gt;This is the direction for the shot.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the interface here is a tag plus a text field. Contextual metadata is editable in the Media Library, in the Upload API, and through the Admin API, so the prompt can come from a human, a spreadsheet, or another workflow. Nobody has to open n8n. If your prompts are house style rather than per-image direction, drop the context condition and hard-code the sentence in the next node instead.&lt;/p&gt;

&lt;p&gt;The full &lt;code&gt;prompt&lt;/code&gt; value on the image above, second sentence included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Slow cinematic push-in with subtle parallax and slight lateral drift. Preserve the image exactly—no new objects or altered details.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Everything else is set once, in one node
&lt;/h3&gt;

&lt;p&gt;The remaining parameters are fixed in the request body, so changing the house look for every future video is a single edit. &lt;code&gt;resolution&lt;/code&gt; takes &lt;code&gt;720p&lt;/code&gt; or &lt;code&gt;1080p&lt;/code&gt;, &lt;code&gt;duration&lt;/code&gt; takes 4, 6, or 8 seconds, and &lt;code&gt;aspect_ratio&lt;/code&gt; takes &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;16:9&lt;/code&gt;, or &lt;code&gt;9:16&lt;/code&gt;. The &lt;a href="https://cloudinary.com/documentation/image_to_video_reference" rel="noopener noreferrer"&gt;image-to-video API reference&lt;/a&gt; has the rest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$json.context.custom.prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image_asset_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$json.asset_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"720p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aspect_ratio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"generate_audio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enhance_prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How the n8n Cloudinary image-to-video workflow works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhnb33y38qgc0mmh6n54x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhnb33y38qgc0mmh6n54x.png" alt="The n8n workflow canvas: Webhook feeds Get Asset, then an Eligible? check. Its true branch runs Start Generation into a Wait 15s, Poll Job, Completed? loop. Completed? true goes to Tag Source Image and Video Player URL; false goes to Give Up?, which either returns to Wait 15s or ends at Stop and Error."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The poll loop is the long connector running back from &lt;strong&gt;Give Up?&lt;/strong&gt; to &lt;strong&gt;Wait 15s&lt;/strong&gt;. Everything above that line is the happy path.&lt;/p&gt;

&lt;p&gt;Eleven nodes, front to back:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Webhook&lt;/strong&gt;: Cloudinary POSTs here on every upload. Register the production URL as your notification endpoint once and forget it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get Asset&lt;/strong&gt;: Fetches the full asset record by &lt;code&gt;asset_id&lt;/code&gt;. The notification payload is thin; tags and context are read from here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eligible?&lt;/strong&gt;: The three conditions above, combined with AND. The false branch is deliberately unwired; most uploads stop here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start Generation&lt;/strong&gt;: POSTs the JSON above to the image-to-video endpoint and gets back a &lt;code&gt;job_id&lt;/code&gt;. Generation is asynchronous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait 15s&lt;/strong&gt;: n8n suspends the execution rather than holding a worker open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poll Job&lt;/strong&gt;: GETs the job by ID and reads &lt;code&gt;data.status&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completed?&lt;/strong&gt;: On &lt;code&gt;completed&lt;/code&gt;, go forward. Anything else goes to the give-up check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give Up?&lt;/strong&gt;: Errors out on &lt;code&gt;failed&lt;/code&gt;, or when &lt;code&gt;$runIndex &amp;gt;= 20&lt;/code&gt;. Otherwise it loops back to &lt;strong&gt;Wait 15s&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop and Error&lt;/strong&gt;: Fails the execution with the job ID and last-seen status, so the n8n execution log tells you which asset to retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag Source Image&lt;/strong&gt;: Appends &lt;code&gt;video_gen_done&lt;/code&gt; to the source image. This is the idempotency marker and the audit trail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video Player URL&lt;/strong&gt;: Builds a Cloudinary Video Player embed URL for the finished asset, muted, looping, and fluid, ready to paste into a CMS.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Watch this line.&lt;/strong&gt; &lt;code&gt;$runIndex &amp;gt;= 20&lt;/code&gt; in &lt;strong&gt;Give Up?&lt;/strong&gt; is the only thing between you and an infinite poll loop. Twenty polls at 15 seconds is a five-minute ceiling. Raise it for longer clips; do not remove it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Delivering the result
&lt;/h2&gt;

&lt;p&gt;The generated MP4 is 1.25 MB at source. Adding &lt;code&gt;f_auto&lt;/code&gt; and &lt;code&gt;q_auto&lt;/code&gt; as the last two components of the delivery URL takes it to 436 KB and lets the CDN pick the codec per browser: AV1 or VP9 where supported, H.264 everywhere else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# video
https://&amp;lt;cloud&amp;gt;.cloudinary.com/video/upload/c_limit,w_1280/f_auto/q_auto/v1787656854/image-to-video/i2v_9f3d....mp4

# source image
https://&amp;lt;cloud&amp;gt;.cloudinary.com/image/upload/c_limit,w_800/f_auto/q_auto/v1787656781/n8n-demo/couple-meme.webp

# poster frame, pulled from the video at t=0
https://&amp;lt;cloud&amp;gt;.cloudinary.com/video/upload/c_limit,w_1280/so_0/f_auto/q_auto/v1787656854/image-to-video/i2v_9f3d....jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MP4 at source&lt;/td&gt;
&lt;td&gt;1.25 MB (1280×720, 4s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With &lt;code&gt;q_auto&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;436 KB, 65% smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poll ceiling&lt;/td&gt;
&lt;td&gt;5 min (20 × 15s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nodes&lt;/td&gt;
&lt;td&gt;11, zero code nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;c_limit&lt;/code&gt; caps the width without ever upscaling, so an asset smaller than the requested width is delivered untouched. Keep &lt;code&gt;f_auto&lt;/code&gt; out of named and incoming transformations: format selection has to resolve per request, at the edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Or skip the URL building entirely
&lt;/h3&gt;

&lt;p&gt;The last node returns a Video Player embed URL, with the playback options already folded in as query parameters. It is a page you can open, link, or drop straight into a CMS embed field; no &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element or player script is needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://player.cloudinary.com/embed/?cloud_name=&amp;lt;cloud&amp;gt;&amp;amp;public_id=image-to-video%2Fi2v_9f3d...&amp;amp;player[loop]=true&amp;amp;player[muted]=true&amp;amp;player[fluid]=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;player[…]&lt;/code&gt; keys map one-to-one onto the node's playback and layout fields, so the muted, looping, fluid-width behaviour is configured in n8n rather than in your front end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Import the n8n Cloudinary image-to-video workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://eitanpeer-res.cloudinary.com/raw/upload/v1787658491/Cloudinary_Image_to_Video_tag__video_gen_SHARED.json" rel="noopener noreferrer"&gt;Download the workflow JSON&lt;/a&gt; and import it through &lt;strong&gt;Workflows → Import from File&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Install the &lt;a href="https://cloudinary.com/documentation/n8n_integration" rel="noopener noreferrer"&gt;Cloudinary node for n8n&lt;/a&gt;: in a new n8n flow, search the nodes panel for &lt;strong&gt;Cloudinary&lt;/strong&gt;, select the integration, and click &lt;strong&gt;Install node&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Create two credentials and re-select them on the nodes marked &lt;code&gt;REPLACE_ME&lt;/code&gt;: a &lt;strong&gt;Cloudinary API&lt;/strong&gt; credential for the three Cloudinary nodes, and an &lt;strong&gt;HTTP Basic Auth&lt;/strong&gt; credential for the two HTTP Request nodes. Its username is your API key and its password is your API secret.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;YOUR_CLOUD_NAME&lt;/code&gt; in the URLs on &lt;strong&gt;Start Generation&lt;/strong&gt; and &lt;strong&gt;Poll Job&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;Eligible?&lt;/strong&gt; and set the conditions to your own rules: the tag name, the metadata field, or whatever else decides that an upload should become a video.&lt;/li&gt;
&lt;li&gt;Copy the production webhook URL and add it in the Cloudinary Console under &lt;strong&gt;Settings → Upload → Notification URL&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Cmd/Ctrl+S&lt;/strong&gt; to save, then activate the workflow. An unsaved workflow will not fire.&lt;/li&gt;
&lt;li&gt;Upload an image with the &lt;code&gt;video_gen&lt;/code&gt; tag and a &lt;code&gt;prompt&lt;/code&gt; context value.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where to take it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aspect-ratio presets by tag.&lt;/strong&gt; Branch on &lt;code&gt;video_gen_9x16&lt;/code&gt; or &lt;code&gt;video_gen_1x1&lt;/code&gt; and set &lt;code&gt;aspect_ratio&lt;/code&gt; per branch, so social formats come out of the same upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the result back.&lt;/strong&gt; Add a node that stores the video's public ID in the source image's context. The pairing then survives outside n8n's execution log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured metadata instead of context.&lt;/strong&gt; A structured metadata field gives the prompt validation and a proper label in the Media Library UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backfill.&lt;/strong&gt; Point the Search API at &lt;code&gt;tags=video_gen AND -tags=video_gen_done&lt;/code&gt; and feed the results into the same chain to catch everything tagged before the workflow existed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell someone.&lt;/strong&gt; A Slack node after &lt;strong&gt;Video Player URL&lt;/strong&gt; posts the player link to the channel that asked for the video.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get a cloud to run it on
&lt;/h2&gt;

&lt;p&gt;Everything here works on a free account. &lt;a href="https://cloudinary.com/users/register_free" rel="noopener noreferrer"&gt;Sign up&lt;/a&gt;, then find the cloud name, API key, and secret on your dashboard. If an agent is doing the setup, it can provision its own environment instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @cloudinary/cloud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command writes working credentials to &lt;code&gt;.env&lt;/code&gt; with no signup. Delivery is locked to the IP the command ran from, and the cloud expires in 24 hours unless someone claims it with an email address. At that point the same credentials become permanent. The &lt;a href="https://cloudinary.com/documentation/agents_start_here" rel="noopener noreferrer"&gt;agent quickstart&lt;/a&gt; covers the claim step and the IP flags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflow JSON:&lt;/strong&gt; &lt;a href="https://eitanpeer-res.cloudinary.com/raw/upload/v1787658491/Cloudinary_Image_to_Video_tag__video_gen_SHARED.json" rel="noopener noreferrer"&gt;Cloudinary — Image to Video (tag: video_gen)&lt;/a&gt;, import-ready, credentials stripped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;n8n integration:&lt;/strong&gt; &lt;a href="https://cloudinary.com/documentation/n8n_integration" rel="noopener noreferrer"&gt;Cloudinary node for n8n&lt;/a&gt;: installation, credentials, and supported operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API reference:&lt;/strong&gt; &lt;a href="https://cloudinary.com/documentation/image_to_video_reference" rel="noopener noreferrer"&gt;Image-to-video API&lt;/a&gt;: every parameter the &lt;strong&gt;Start Generation&lt;/strong&gt; node sends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://cloudinary.com/documentation/video_optimization" rel="noopener noreferrer"&gt;Video optimization&lt;/a&gt; · &lt;a href="https://cloudinary.com/documentation/notifications" rel="noopener noreferrer"&gt;Upload notifications&lt;/a&gt; · &lt;a href="https://cloudinary.com/documentation/video_player_how_to_embed" rel="noopener noreferrer"&gt;Video Player embed&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free account:&lt;/strong&gt; &lt;a href="https://cloudinary.com/users/register_free" rel="noopener noreferrer"&gt;cloudinary.com/users/register_free&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For agents:&lt;/strong&gt; &lt;a href="https://cloudinary.com/documentation/agents_start_here" rel="noopener noreferrer"&gt;Claimable clouds&lt;/a&gt;: &lt;code&gt;npx @cloudinary/cloud&lt;/code&gt;, no signup.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Cloudinary ❤️ developers&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👉 &lt;strong&gt;&lt;a href="https://link.cloudinary.com/usq8w" rel="noopener noreferrer"&gt;Create your free account&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>n8n</category>
      <category>cloudinary</category>
      <category>automation</category>
      <category>ai</category>
    </item>
    <item>
      <title>My Agent Built a Paid Media Storefront Before I Signed Up for Anything</title>
      <dc:creator>Nick Bradley</dc:creator>
      <pubDate>Wed, 12 Aug 2026 12:53:25 +0000</pubDate>
      <link>https://dev.to/cloudinary/my-agent-built-a-paid-media-storefront-before-i-signed-up-for-anything-2k47</link>
      <guid>https://dev.to/cloudinary/my-agent-built-a-paid-media-storefront-before-i-signed-up-for-anything-2k47</guid>
      <description>&lt;p&gt;Here's a build log that would have been impossible a year ago.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I told an agent to clone &lt;a href="https://github.com/cloudinary-devs/unshutter" rel="noopener noreferrer"&gt;unshutter&lt;/a&gt; and get it running.&lt;/li&gt;
&lt;li&gt;It provisioned a Cloudinary Claimable Cloud, a Neon Postgres database and a Stripe sandbox, &lt;strong&gt;without me creating a single account.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;It configured the media pipeline, then ran a real test purchase end to end: watermarked preview → Stripe Checkout → signed download of the original.&lt;/li&gt;
&lt;li&gt;It handed me two claim URLs and stopped.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I claimed the environments the way you review a PR: after seeing the working result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6kqo3udt5myswfvxnyy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6kqo3udt5myswfvxnyy.webp" alt="The unshutter gallery: watermarked previews on a warm gallery wall, originals locked behind the media layer" width="799" height="635"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The storefront is just the demo. What I actually want to talk about is the provisioning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one step agents couldn't do
&lt;/h2&gt;

&lt;p&gt;Agents don't evaluate a product the way we do. They don't read the landing page or compare the pricing table. They want to &lt;em&gt;try it&lt;/em&gt;: spin something up, build against it, run the result, decide.&lt;/p&gt;

&lt;p&gt;Until recently every platform had exactly one step that blocked that: &lt;strong&gt;creating the account.&lt;/strong&gt; A signup form, an email confirmation, a dashboard to fish an API key out of. Ten minutes of human clicking standing in front of hours of autonomous work.&lt;/p&gt;

&lt;p&gt;That step is gone across this whole stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @cloudinary/cloud        &lt;span class="c"&gt;# Claimable Cloud → CLOUDINARY_URL in .env, claim URL, 24h&lt;/span&gt;
npx neon-new &lt;span class="nt"&gt;--yes&lt;/span&gt;           &lt;span class="c"&gt;# Claimable Postgres → connection string, claim URL, 72h&lt;/span&gt;
stripe sandbox create        &lt;span class="c"&gt;# anonymous sandbox → working test keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Three commands, no signups, and each one returns working credentials plus a claim link.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloudinary.com/documentation/claimable_cloud_provisioning" rel="noopener noreferrer"&gt;Cloudinary's version&lt;/a&gt; returns something like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;CLOUD_NAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;a1b2c3d4&lt;/span&gt;
&lt;span class="py"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;123456789012345&lt;/span&gt;
&lt;span class="py"&gt;API_SECRET&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;«redacted»&lt;/span&gt;
&lt;span class="py"&gt;CLOUDINARY_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;cloudinary://123456789012345:«redacted»@a1b2c3d4&lt;/span&gt;
&lt;span class="py"&gt;EXPIRES_AT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2026-08-06T08:29:07Z&lt;/span&gt;
&lt;span class="py"&gt;CLAIM_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://cloudinary.com/users/agent_email_confirmation?token=«redacted»&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It writes &lt;code&gt;CLOUDINARY_URL&lt;/code&gt; straight into &lt;code&gt;.env&lt;/code&gt; and the whole platform is live from that first call: uploads, transformations, AI add-ons, CDN delivery. There's no trial tier with a queue in front of it.&lt;/p&gt;

&lt;p&gt;Two properties matter for what follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unclaimed, it expires in 24 hours.&lt;/strong&gt; Claiming it (email confirmation on that URL) converts it to a permanent free account, with the same cloud name, the same credentials and every uploaded asset intact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unclaimed, media delivery is locked to a public IP&lt;/strong&gt;, by default the one the command ran from. You can name up to three at provisioning time by repeating &lt;code&gt;--ip&lt;/code&gt;, which is enough for your laptop plus a preview host, but not enough for the open internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The second one is the interesting bit: &lt;strong&gt;the claim is the go-public switch.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent's build works from the IPs it was provisioned for. Claiming is the moment it can serve media to anyone, anywhere. That's why the agent stops at a working local site instead of deploying: deploy before claiming and you've published something that looks broken to every visitor but you. Claim, then deploy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fry13aj8f1rhsk1h5gbd6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fry13aj8f1rhsk1h5gbd6.jpeg" alt="Agent provisioning diagram" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What it built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/cloudinary-devs/unshutter" rel="noopener noreferrer"&gt;unshutter&lt;/a&gt; &lt;em&gt;(v.: to open the shutters; to let the light in)&lt;/em&gt; is a stock-media storefront selling photos, videos and photosets. Next.js 16, Payload CMS, Neon, Stripe, Cloudinary, MIT-licensed. Every mechanic below links to the file that implements it, so you can check my work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The paywall is enforced by the media layer, not by hiding URLs.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Originals upload as restricted assets, &lt;code&gt;type: private&lt;/code&gt; for images and &lt;code&gt;type: authenticated&lt;/code&gt; for video (&lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/src/lib/cloudinary/uploadOriginal.ts" rel="noopener noreferrer"&gt;&lt;code&gt;uploadOriginal.ts&lt;/code&gt;&lt;/a&gt;). The CDN refuses to deliver them unsigned.&lt;/li&gt;
&lt;li&gt;With Strict Transformations on, only allowlisted transformations resolve publicly (&lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/src/components/PreviewImage.tsx" rel="noopener noreferrer"&gt;&lt;code&gt;PreviewImage.tsx&lt;/code&gt;&lt;/a&gt; is the one component allowed to render them). There's no URL you can craft to get the un-watermarked variant, and what the public &lt;em&gt;can&lt;/em&gt; see is deliberately degraded: watermark, &lt;code&gt;q_auto:low&lt;/code&gt;, size-capped.&lt;/li&gt;
&lt;li&gt;Paying mints a ~10-minute signed download URL (&lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/src/lib/cloudinary/buildDownloadUrl.ts" rel="noopener noreferrer"&gt;&lt;code&gt;buildDownloadUrl.ts&lt;/code&gt;&lt;/a&gt;). &lt;strong&gt;Stripe's payment record &lt;em&gt;is&lt;/em&gt; the entitlement&lt;/strong&gt;, checked with one live API call per visit (&lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/src/lib/stripe/verifySession.ts" rel="noopener noreferrer"&gt;&lt;code&gt;verifySession.ts&lt;/code&gt;&lt;/a&gt;). No user table, no entitlement table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Videos lean on &lt;code&gt;e_preview&lt;/code&gt; for an AI-picked five-second clip plus six filmstrip stills (&lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/src/lib/cloudinary/videoTransformations.ts" rel="noopener noreferrer"&gt;&lt;code&gt;videoTransformations.ts&lt;/code&gt;&lt;/a&gt;). Photosets sell a folder as one product, delivered as a signed, expiring zip that &lt;code&gt;generate_archive&lt;/code&gt; assembles on the fly (&lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/src/lib/cloudinary/buildPhotosetZipUrl.ts" rel="noopener noreferrer"&gt;&lt;code&gt;buildPhotosetZipUrl.ts&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/scripts/ingest-photoset.ts" rel="noopener noreferrer"&gt;ingest script&lt;/a&gt;).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;tbody&gt;
&lt;tr&gt;
    &lt;td width="50%"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fcloudinary-devs%2Funshutter%2Fmain%2Fdocs%2Fpromo%2Fvideo.webp" alt="A video product page with an AI-picked 5-second preview clip and a six-frame watermarked filmstrip" width="799" height="635"&gt;&lt;/td&gt;
    &lt;td width="50%"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fcloudinary-devs%2Funshutter%2Fmain%2Fdocs%2Fpromo%2Fphotoset.webp" alt="A photoset product page: a folder of originals behind curated previews, sold as one zip" width="799" height="635"&gt;&lt;/td&gt;
  &lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's worth reading even if you never run it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/src/lib/cloudinary/README.md" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;src/lib/cloudinary/README.md&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;: the honest version of this section. Every named transformation with its exact recipe, why &lt;code&gt;f_auto&lt;/code&gt; has to be chained outside the named transformation, and the trade-offs I accepted rather than solved. Every URL builder sits in &lt;a href="https://github.com/cloudinary-devs/unshutter/tree/main/src/lib/cloudinary" rel="noopener noreferrer"&gt;&lt;code&gt;src/lib/cloudinary/&lt;/code&gt;&lt;/a&gt; with unit tests beside it.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/docs/ARCHITECTURE.md" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;docs/ARCHITECTURE.md&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;: how Payload, Neon, Stripe and Cloudinary divide the work, plus &lt;a href="https://github.com/cloudinary-devs/unshutter/tree/main/src/collections" rel="noopener noreferrer"&gt;&lt;code&gt;src/collections/&lt;/code&gt;&lt;/a&gt; for the CMS hooks that upload on save and destroy on delete.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/src/site.config.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/site.config.ts&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/src/styles/unshutter-tokens.css" rel="noopener noreferrer"&gt;&lt;code&gt;unshutter-tokens.css&lt;/code&gt;&lt;/a&gt;: rebranding it as your own storefront is those two files and no component edits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Read the code: &lt;a href="https://github.com/cloudinary-devs/unshutter" rel="noopener noreferrer"&gt;github.com/cloudinary-devs/unshutter&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every one of those mechanics is configuration that has to exist &lt;em&gt;inside&lt;/em&gt; the media account: named transformations, strict-mode allowlists, delivery types, signed archives. That's the real test of an anonymously provisioned one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Could it actually do the work?
&lt;/h2&gt;

&lt;p&gt;Provisioning an empty environment is the easy half. The question is whether an agent can stand all that up in one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It configured the pipeline.&lt;/strong&gt; Four named transformations created over the Admin API (the watermark chain, OG images, the AI video preview, the filmstrip stills), each flagged &lt;code&gt;allowed_for_strict: true&lt;/code&gt;. No console, no clicking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It verified the security model instead of assuming it.&lt;/strong&gt; Uploaded test media, then requested a raw original URL unsigned and confirmed the 401. That check matters: if Strict Transformations happens to be off, the watermark is bypassable and everything above is theatre. So the agent proves it and reports what it found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It ran the money path.&lt;/strong&gt; Browse → cart → Stripe Checkout with &lt;code&gt;4242 4242 4242 4242&lt;/code&gt; → webhook → &lt;code&gt;/download&lt;/code&gt; → signed URL → un-watermarked original on disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then it stopped&lt;/strong&gt; and told me what existed and what needed claiming.&lt;/p&gt;

&lt;p&gt;All of it ran against live infrastructure rather than mocks. When it needed current facts rather than whatever was in its training data, it read Cloudinary's &lt;a href="https://cloudinary.com/documentation/agents_start_here" rel="noopener noreferrer"&gt;agent docs&lt;/a&gt; and machine-readable &lt;a href="https://cloudinary.com/documentation/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt;, the same entry points any agent can load.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the human went
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;Now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Get three live environments&lt;/td&gt;
&lt;td&gt;✋ three signup forms, dashboard key hunts&lt;/td&gt;
&lt;td&gt;🤖 three commands, no accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write the credentials into &lt;code&gt;.env&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✋ copy-paste&lt;/td&gt;
&lt;td&gt;🤖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configure the media pipeline&lt;/td&gt;
&lt;td&gt;✋ console clicking&lt;/td&gt;
&lt;td&gt;🤖 Admin API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify the paywall holds&lt;/td&gt;
&lt;td&gt;✋ hope&lt;/td&gt;
&lt;td&gt;🤖 tested, reported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Own the environments&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✋ &lt;strong&gt;first&lt;/strong&gt;, before a line of code ran&lt;/td&gt;
&lt;td&gt;✋ &lt;strong&gt;last&lt;/strong&gt;, confirm an email, ≤24h / ≤72h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy it&lt;/td&gt;
&lt;td&gt;✋&lt;/td&gt;
&lt;td&gt;✋ your account, your call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decide it's real money time&lt;/td&gt;
&lt;td&gt;✋&lt;/td&gt;
&lt;td&gt;✋&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read that middle row on its own: the email confirmation didn't disappear, it just stopped being step zero. Same click, opposite end of the process. By the time you make it, you're confirming something you've already watched work.&lt;/p&gt;

&lt;p&gt;The human touches didn't only shrink, they &lt;strong&gt;moved to after the work&lt;/strong&gt;, where they feel like decisions rather than chores. What's left is ownership, infrastructure and judgment, which is roughly what should need a person anyway.&lt;/p&gt;
&lt;h2&gt;
  
  
  Things that bit us
&lt;/h2&gt;

&lt;p&gt;None of this was completely frictionless but the agent was able to route around it. Some things worth knowing before you try it:&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Already have a Cloudinary account to use? You don't have to provision a Claimable Cloud (but you might want to)
  &lt;p&gt;Claimable Clouds make it easy to provision a working environment, but claiming one means confirming an email address. If that address already belongs to a Cloudinary account, the claim page rejects it. So a claimable cloud you provision for real work can't be folded into the account you already have.&lt;/p&gt;

&lt;p&gt;If you're already a customer, just hand the agent credentials from your own account.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  The IP lock surprises you at deploy time
  &lt;p&gt;An unclaimed cloud delivers media only to the IPs it was provisioned for, by default just the machine that ran the command. Locally everything is perfect; deploy it and the site is fine &lt;em&gt;for you&lt;/em&gt; and broken for everyone else. You can allow up to three addresses at provisioning time by repeating &lt;code&gt;--ip&lt;/code&gt; (handy for a laptop plus a preview host, and &lt;code&gt;requester_ip&lt;/code&gt; is accepted as a literal), but going properly public means claiming, which lifts the restriction entirely. So: claim before you deploy, and if you're scripting this, have your agent report the claim URL loudly rather than burying it in a summary.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  &lt;code&gt;e_preview&lt;/code&gt; can't generate on the fly
  &lt;p&gt;Request it in a plain delivery URL and you get an HTTP 423 until the derived asset exists. It has to be eager-generated at upload with &lt;code&gt;eager_async: true&lt;/code&gt;, plus a signature-verified webhook to flip a &lt;code&gt;previewReady&lt;/code&gt; flag when Cloudinary finishes. Also: the source needs to be longer than the excerpt, so a 3-second clip fails silently.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Cloudinary can't POST a webhook to localhost
  &lt;p&gt;Which means the "preview ready" callback never lands during local development. Either tunnel (ngrok/cloudflared) or flip the flag manually; the repo does the latter as a documented dev fallback.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Two settings still have no API
  &lt;p&gt;Strict Transformations and the AI add-on registrations are account-posture settings you toggle in the console. The add-ons don't block anything: photo ingest retries without them and lands the image untagged. Strict mode does matter, which is why the agent tests for it rather than trusting it.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Point an agent at the repo:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Clone cloudinary-devs/unshutter and get it running locally, using claimable environments. Don't ask me for credentials, don't deploy anywhere. Give me the local URL and the claim URLs when you're done.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or run the three commands yourself and follow the &lt;a href="https://github.com/cloudinary-devs/unshutter#quick-start" rel="noopener noreferrer"&gt;quick start&lt;/a&gt;.&lt;/p&gt;




&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/cloudinary-devs" rel="noopener noreferrer"&gt;
        cloudinary-devs
      &lt;/a&gt; / &lt;a href="https://github.com/cloudinary-devs/unshutter" rel="noopener noreferrer"&gt;
        unshutter
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Photography paywall storefront. An open-source showcase demonstrating Cloudinary in a modern full-stack app - Cloudinary + Next.js 16 + Payload CMS + Neon + Stripe + Vercel
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;unshutter&lt;/h1&gt;
&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;unshutter (v.): to open the shutters; to let the light in.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A stock-media storefront an AI agent can take from &lt;code&gt;git clone&lt;/code&gt; to a running, verified store — media, database, payments — &lt;strong&gt;without a single human signup&lt;/strong&gt;. Every platform in the stack provisions claim-later environments (&lt;code&gt;npx @cloudinary/cloud&lt;/code&gt;, &lt;code&gt;npx neon-new&lt;/code&gt;, &lt;code&gt;stripe sandbox create&lt;/code&gt;), so the human touches move to where they belong: claiming something that already works. Paste the &lt;a href="https://github.com/cloudinary-devs/unshutter#agent-quick-start" rel="noopener noreferrer"&gt;Agent quick start&lt;/a&gt; into your agent and watch.&lt;/p&gt;

&lt;p&gt;Built on &lt;strong&gt;Next.js 16 + Payload CMS + Neon + Stripe + Vercel&lt;/strong&gt;, with &lt;a href="https://cloudinary.com" rel="nofollow noopener noreferrer"&gt;Cloudinary&lt;/a&gt; as the media layer. Sibling to &lt;a href="https://github.com/cloudinary-devs/kickoff-cards" rel="noopener noreferrer"&gt;kickoff-cards&lt;/a&gt; (Cloudinary + Supabase).&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/cloudinary-devs/unshutter/docs/promo/gallery.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fcloudinary-devs%2Funshutter%2FHEAD%2Fdocs%2Fpromo%2Fgallery.webp" alt="The unshutter gallery — watermarked previews on a warm gallery wall, originals locked behind the media layer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three product shapes:&lt;/p&gt;


&lt;ul&gt;

&lt;li&gt;

&lt;strong&gt;Photos&lt;/strong&gt; — browse watermarked, heavily compressed &lt;strong&gt;Previews&lt;/strong&gt;; paying &lt;strong&gt;unshutters&lt;/strong&gt; the image with a time-limited signed link to the full-size &lt;strong&gt;Original&lt;/strong&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Videos&lt;/strong&gt; — an AI-selected 5-second watermarked, low-res preview clip (Cloudinary &lt;code&gt;e_preview&lt;/code&gt;) plus six auto-extracted watermarked stills; purchase…&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/cloudinary-devs/unshutter" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;





&lt;p&gt;The repo ships the guardrails that made agent-building work on a stack this fiddly: &lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/AGENTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/a&gt; working agreements, a &lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/docs/AGENT-PROVISIONING.md" rel="noopener noreferrer"&gt;provisioning walkthrough&lt;/a&gt; with every human touch called out, and the &lt;a href="https://github.com/cloudinary-devs/unshutter/blob/main/docs/VERIFIED.md" rel="noopener noreferrer"&gt;verified platform facts&lt;/a&gt; the implementation rests on. Sibling project, same idea with Supabase: &lt;a href="https://github.com/cloudinary-devs/kickoff-cards" rel="noopener noreferrer"&gt;Kickoff Cards&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Further reading: &lt;a href="https://cloudinary.com/documentation/claimable_cloud_provisioning" rel="noopener noreferrer"&gt;Claimable Cloud provisioning&lt;/a&gt; · &lt;a href="https://cloudinary.com/documentation/agents_start_here" rel="noopener noreferrer"&gt;Agents, start here&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Most of the media plumbing is the agent's job now. What was left for me was deciding whether to keep what it built.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Cloudinary ❤️ developers&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👉 &lt;strong&gt;&lt;a href="https://link.cloudinary.com/ur9jo" rel="noopener noreferrer"&gt;Create your free account&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>nextjs</category>
      <category>cloudinary</category>
    </item>
    <item>
      <title>Resize One Image into 6 Social Media Formats Automatically Using Cloudinary Claimable Clouds</title>
      <dc:creator>Jen Looper</dc:creator>
      <pubDate>Wed, 05 Aug 2026 21:30:34 +0000</pubDate>
      <link>https://dev.to/cloudinary/resize-one-image-into-6-social-media-formats-automatically-using-cloudinary-claimable-clouds-3gfc</link>
      <guid>https://dev.to/cloudinary/resize-one-image-into-6-social-media-formats-automatically-using-cloudinary-claimable-clouds-3gfc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Claimable Clouds are temporary Cloudinary environments for AI workflows that let AI Agents safely manage media with no signup required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine you're a busy designer, with many satisfied clients who depend on you to take their images and make them look great across social media. All that manual cropping and scaling, it's enough to make a body cry.&lt;/p&gt;

&lt;p&gt;On top of that, you know that AI can give you a hand here, but managing the handoff between your AI, your own skilled hands and artistic taste and style, and your always-in-a-hurry client list is another big pain.&lt;/p&gt;

&lt;p&gt;Enter the concept of the Cloudinary Claimable Cloud, just released today.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://cloudinary.com/documentation/claimable_cloud_provisioning" rel="noopener noreferrer"&gt;Take a look at the docs&lt;/a&gt; about these new temporary instances available now&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What we built and why
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Provision a disposable Cloudinary cloud with no signup, using &lt;code&gt;npx @cloudinary/cloud&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Auto-detect a dropped image and upload it to that temporary cloud&lt;/li&gt;
&lt;li&gt;Auto-crop it into 6+ social formats (Instagram, LinkedIn, X, Facebook, Stories) using AI-based smart cropping&lt;/li&gt;
&lt;li&gt;Generate a side-by-side gallery of results automatically&lt;/li&gt;
&lt;li&gt;Hand off a Claim URL so a client can make the cloud permanent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, you can hand off the main pain points to AI - the resizing and reshaping of your images for the various social media platforms, while giving your clients a clean handoff via a temporary Cloud environment that they can use to create a Cloudinary account and start using these assets.&lt;/p&gt;

&lt;p&gt;One side effect: this also nudges your whole client base toward the same toolset - Cloudinary. The bigger deal is working with an AI agent that makes your life easier but ALSO allows you to keep control of the output. &lt;/p&gt;

&lt;p&gt;Let's walk through how this works! It all boils down to a new command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @cloudinary/cloud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type that into your terminal to kick off the process. &lt;/p&gt;

&lt;p&gt;I built a &lt;a href="http://github.com/jlooper/social-me" rel="noopener noreferrer"&gt;small app&lt;/a&gt; around this concept to provide this AI agent with a simple harness, so let me show how that looks. The user experience is to drop any image you want resized into the &lt;code&gt;/drop&lt;/code&gt; folder. Under the cover, there are a few more processes at work: &lt;/p&gt;

&lt;h2&gt;
  
  
  Provisioning: Spin Up a Temporary Cloudinary Cloud
&lt;/h2&gt;

&lt;p&gt;On the first run, the app spins up a disposable Cloudinary "Claimable Cloud" via &lt;code&gt;npx @cloudinary/cloud&lt;/code&gt; with temporary credentials, so no signup is required. The generated credentials are saved temporarily in a .env file locally. This gives the tool a live upload/delivery endpoint without any manual account setup. These credentials are refreshed when you start the app: &lt;code&gt;npm i &amp;amp;&amp;amp; npm start&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch &amp;amp; Transform: Auto-Crop for Each Social Platform
&lt;/h2&gt;

&lt;p&gt;The app watches the &lt;code&gt;drop&lt;/code&gt; folder for new images. When a high-resolution master image lands there, it's uploaded to this temporary Cloudinary instance. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffnb9cfto74uahxogcb89.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffnb9cfto74uahxogcb89.jpeg" alt="a Nano Banana image" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The image we want to resize&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then it's run through a set of predefined social-media format specs (Instagram Square, Portrait, Story/Reels/TikTok, LinkedIn, X/Twitter, Facebook) using dynamic transformations like c_fill,g_auto for smart auto-cropping to each platform's exact aspect ratio and dimensions, focusing on the most important element. Control these formats and transformations in the &lt;code&gt;src/formats.js&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FORMATS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instagram-square&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Instagram Square Post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1:1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;buildTransformation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;subjectFill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instagram-portrait&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Instagram Feed Portrait&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4:5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;buildTransformation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;subjectFill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1350&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instagram-story&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Instagram Story / Reels / TikTok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;9:16&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;buildTransformation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;padWithBlurredBackground&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Render Results &amp;amp; Send to Client: Share a Claim URL
&lt;/h2&gt;

&lt;p&gt;The temporary Claimable Cloud is provisioned using the temporary credentials stored in .env:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Ensures a usable CLOUDINARY_URL is in process.env, provisioning a disposable&lt;/span&gt;
&lt;span class="c1"&gt;// cloud on demand. Returns { claimUrl } (claimUrl is undefined when a cloud&lt;/span&gt;
&lt;span class="c1"&gt;// was already configured before this run).&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ensureCloud&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;loadEnv&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLOUDINARY_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;claimUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLOUDINARY_CLOUD_CLAIM_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;provisioned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No Cloudinary cloud configured yet — provisioning a disposable one (no signup needed)...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-y&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cloudinary/cloud&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--no-agent-metadata&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;combined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stderr&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;redact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseKeyValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;loadEnv&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLOUDINARY_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cloud provisioning did not produce a CLOUDINARY_URL.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;claimUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLAIM_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;provisioned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generated variant URLs are written to manifest.json and rendered into an auto-generated side-by-side HTML gallery, which opens automatically. The output also includes a Claim URL, letting the disposable cloud be handed off (e.g., to a client) and made permanent at any time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvntpjxpac7lcfhshn7z4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvntpjxpac7lcfhshn7z4.png" alt="The results" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The temporary link and generated gallery of images to pick from. Note the fancy Instagram one!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now you can pass that Claim URL right on to your client and start the process of allowing them to claim the results and start working with them. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmqmu2njweqs5sqfsd2xv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmqmu2njweqs5sqfsd2xv.png" alt="login box for claimable cloud" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is an early feature, with more agent-based tools coming from Cloudinary soon. Watch for more interesting work around AI agents in the near future!&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Cloudinary ❤️ developers&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👉 &lt;strong&gt;&lt;a href="https://link.cloudinary.com/ur1AT" rel="noopener noreferrer"&gt;Create your free account&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Up and Running with Cloudinary’s Next Starter Kit</title>
      <dc:creator>Eric Portis</dc:creator>
      <pubDate>Mon, 03 Aug 2026 22:17:13 +0000</pubDate>
      <link>https://dev.to/cloudinary/up-and-running-with-cloudinarys-next-starter-kit-4586</link>
      <guid>https://dev.to/cloudinary/up-and-running-with-cloudinarys-next-starter-kit-4586</guid>
      <description>&lt;p&gt;Back in April, &lt;a href="https://cloudinary.com/blog/web-app-create-cloudinary-react" rel="noopener noreferrer"&gt;we released&lt;/a&gt; our &lt;a href="https://github.com/cloudinary-devs/create-cloudinary-react" rel="noopener noreferrer"&gt;React Starter Kit&lt;/a&gt;. The React Starter Kit allows you to type one command in the terminal, answer a few questions, and scaffold out a complete, Cloudinary-powered React application, hooked up to all of Cloudinary's latest and greatest tooling for agentic coding workflows.&lt;/p&gt;

&lt;p&gt;Today we're releasing &lt;a href="https://github.com/cloudinary-devs/create-cloudinary-next" rel="noopener noreferrer"&gt;a sequel&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-cloudinary-next
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're spinning up a fresh, media-driven &lt;a href="https://nextjs.org" rel="noopener noreferrer"&gt;&lt;em&gt;Next.js&lt;/em&gt;&lt;/a&gt; application, we think typing those 26 characters into your terminal and hitting &lt;code&gt;return&lt;/code&gt; is the best way to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick demo of Cloudinary's Next Starter Kit
&lt;/h2&gt;

&lt;p&gt;Here's a quick example of the kind of app this allows you to quickly prototype.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Be7aR-Z_NCE"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cloudinary's Next Starter Kit actually does
&lt;/h2&gt;

&lt;p&gt;What does the Next Starter Kit actually do?&lt;/p&gt;

&lt;p&gt;First, it asks you a few questions about your Cloudinary account and development environment. If you don't have the necessary prerequisites in place, it'll guide you through the process of creating them. Once you do, it records your answers in specific environment variables, configuring your app to work with your Cloudinary account.&lt;/p&gt;

&lt;p&gt;Then, it runs &lt;a href="https://nextjs.org/docs/app/api-reference/cli/create-next-app" rel="noopener noreferrer"&gt;create-next-app&lt;/a&gt;, installing Next.js and spinning up a working Next app using our &lt;a href="https://github.com/cloudinary-devs/create-cloudinary-next-template" rel="noopener noreferrer"&gt;starter template&lt;/a&gt;. The template includes our Next.js library and demonstrates use of our official Next.js components for upload and delivery, and suggests a handful of prompts to get you started building.&lt;/p&gt;

&lt;p&gt;Last, depending which AI coding assistants you told the Starter Kit that you were using during setup, it configures and installs the relevant Cloudinary &lt;a href="https://github.com/cloudinary/mcp-servers" rel="noopener noreferrer"&gt;MCP servers&lt;/a&gt; and &lt;a href="https://github.com/cloudinary-devs/skills" rel="noopener noreferrer"&gt;skills&lt;/a&gt; where your assistants can find them.&lt;/p&gt;

&lt;p&gt;The end result is a running Next.js app that can upload and deliver media from your Cloudinary account using our &lt;a href="https://cloudinary.com/documentation/nextjs_integration#use" rel="noopener noreferrer"&gt;official Next.js components&lt;/a&gt;, and a development environment primed for prompting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cloudinary Next.js skill
&lt;/h2&gt;

&lt;p&gt;The heart and soul of the agentic story here is our new &lt;a href="https://github.com/cloudinary-devs/skills/tree/main/skills/cloudinary-next" rel="noopener noreferrer"&gt;Next.js skill&lt;/a&gt;. The skill is the result of a mountain of user research. Late last year, my colleague Raya Straus spent weeks talking to users, poring over support tickets and error logs, and soliciting reviews from internal experts. The result is a bunch of advice about how to be successful when using Cloudinary generally and our Next.js library specifically, distilled into an installable skill. When paired with our existing &lt;a href="https://github.com/cloudinary-devs/skills/tree/main/skills/cloudinary-docs" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; and &lt;a href="https://github.com/cloudinary-devs/skills/tree/main/skills/cloudinary-transformations" rel="noopener noreferrer"&gt;transformation&lt;/a&gt; skills, it helps agentic tooling succeed with Cloudinary and catch common errors.&lt;/p&gt;

&lt;p&gt;We're excited to launch the skill alongside the Starter Kit, and to keep iterating on both based on automated evals and feedback from users like you.&lt;/p&gt;

&lt;h2&gt;
  
  
  And you're off
&lt;/h2&gt;

&lt;p&gt;So! if you're prototyping or building a new media-driven Next.js app, start here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-cloudinary-next
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And please, let us know what you think in the comments!  &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Cloudinary ❤️ developers&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👉 &lt;strong&gt;&lt;a href="https://link.cloudinary.com/urXbL" rel="noopener noreferrer"&gt;Create your free account&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>javascript</category>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>No Brief. No Limits. Five Builds Powered by the Cloudinary Skills Pack</title>
      <dc:creator>eugene musebe</dc:creator>
      <pubDate>Sun, 26 Jul 2026 19:54:23 +0000</pubDate>
      <link>https://dev.to/cloudinary/no-brief-no-limits-five-builds-powered-by-the-cloudinary-skills-pack-425a</link>
      <guid>https://dev.to/cloudinary/no-brief-no-limits-five-builds-powered-by-the-cloudinary-skills-pack-425a</guid>
      <description>&lt;p&gt;Our &lt;a href="https://dev.to/cloudinary/5-projects-2-weeks-1-goal-building-a-more-sustainable-web-2gc8"&gt;last Mini-Hack&lt;/a&gt; started with a constraint.&lt;/p&gt;

&lt;p&gt;Build something that makes the web lighter, faster, and more sustainable.&lt;/p&gt;

&lt;p&gt;For June, we tried the opposite.&lt;/p&gt;

&lt;p&gt;No theme. No prescribed problem. No expectation that every submission should look or work the same.&lt;/p&gt;

&lt;p&gt;We gave the &lt;a href="https://cloudinary.com/developers/community" rel="noopener noreferrer"&gt;Cloudinary Creators community&lt;/a&gt; a new toolkit, one simple requirement, and as much creative freedom as possible:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use at least one skill from the &lt;a href="https://cloudinary.com/documentation/cloudinary_llm_mcp#cloudinary_skills" rel="noopener noreferrer"&gt;Cloudinary Skills Pack&lt;/a&gt; and build something real.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Solo or with a partner. Small experiment or ambitious product. Developer tool, creative platform, healthcare app, media editor, or something we had never considered.&lt;/p&gt;

&lt;p&gt;The result was five projects that went in five completely different directions.&lt;/p&gt;

&lt;p&gt;That was exactly the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Brief Was Almost No Brief
&lt;/h2&gt;

&lt;p&gt;The challenge was called &lt;strong&gt;Open Innovation with the Cloudinary Skills Pack&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The instructions were deliberately simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build anything you want.&lt;/li&gt;
&lt;li&gt;Use at least one Cloudinary AI skill.&lt;/li&gt;
&lt;li&gt;Focus on utility.&lt;/li&gt;
&lt;li&gt;Keep the scope manageable.&lt;/li&gt;
&lt;li&gt;Show the community what you made.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Creators could submit a live demo, public repository, project write-up, or walkthrough video.&lt;/p&gt;

&lt;p&gt;There was no shared industry, audience, or use case. We wanted creators to start with problems they personally cared about, then explore how Cloudinary could help them solve those problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet the Cloudinary Skills Pack
&lt;/h2&gt;

&lt;p&gt;The Mini-Hack centered on the newly launched &lt;strong&gt;&lt;a href="https://cloudinary.com/documentation/cloudinary_llm_mcp#cloudinary_skills" rel="noopener noreferrer"&gt;Cloudinary Skills Pack&lt;/a&gt;&lt;/strong&gt;, a collection of agent skills designed to help AI coding tools work more accurately with images, videos, transformations, and Cloudinary SDKs.&lt;/p&gt;

&lt;p&gt;The pack included three skills:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;cloudinary-docs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Grounds AI agents in Cloudinary documentation so they can provide more accurate implementation guidance instead of relying on outdated patterns or guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;cloudinary-transformations&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Turns natural-language transformation requests into valid Cloudinary image and video transformation URLs.&lt;/p&gt;

&lt;p&gt;Describe what you want, such as removing a background, resizing an image, generating a crop, or applying an effect, and the skill helps translate that request into a usable transformation.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;cloudinary-react&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Provides current Cloudinary React SDK guidance, including component patterns, package usage, and correct import paths.&lt;/p&gt;

&lt;p&gt;Together, the skills give AI coding agents a better understanding of how media workflows should actually be built.&lt;/p&gt;

&lt;p&gt;Our pitch to the community was simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here is a toolkit for building AI experiences that understand media. Now show us a use case we have not thought of yet.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Support Along the Way
&lt;/h2&gt;

&lt;p&gt;Open-ended challenges create freedom, but they can also create uncertainty.&lt;/p&gt;

&lt;p&gt;To help creators move from ideas to finished projects, we hosted a live Office Hours session on Discord. Participants could bring Skills Pack questions, implementation problems, deployment issues, or unfinished ideas.&lt;/p&gt;

&lt;p&gt;Community members and mentors worked through errors, discussed project scope, and helped builders polish their submissions before the deadline.&lt;/p&gt;

&lt;p&gt;Then came the final push.&lt;/p&gt;

&lt;p&gt;Submissions officially closed on July 5.&lt;/p&gt;

&lt;p&gt;Participants were competing for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Cloudinary swag pack&lt;/li&gt;
&lt;li&gt;AI credits for future projects&lt;/li&gt;
&lt;li&gt;A featured place in the Cloudinary Community Showcase&lt;/li&gt;
&lt;li&gt;Promotion across Cloudinary social channels&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Five Projects, Five Completely Different Directions
&lt;/h2&gt;

&lt;p&gt;Removing the theme did not make the projects feel disconnected.&lt;/p&gt;

&lt;p&gt;Instead, it revealed what our creators were already interested in solving.&lt;/p&gt;

&lt;p&gt;They built media editors, optimization platforms, recovery journals, developer copilots, and branding assistants.&lt;/p&gt;

&lt;p&gt;The projects shared the same toolkit, but almost nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  CloudCanvas, by Joyston
&lt;/h2&gt;

&lt;p&gt;What would a Photoshop or Canva-style editor look like if Cloudinary powered the entire media workflow?&lt;/p&gt;

&lt;p&gt;That is the question behind &lt;strong&gt;CloudCanvas&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;CloudCanvas is an AI-powered image editor that runs directly in the browser. It uses a non-destructive, layer-based canvas, allowing users to upload images, generate new visuals, apply edits, and export optimized assets without leaving the application.&lt;/p&gt;

&lt;p&gt;Its editing tools include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smart Expand&lt;/li&gt;
&lt;li&gt;Object Removal&lt;/li&gt;
&lt;li&gt;Background Removal&lt;/li&gt;
&lt;li&gt;AI Restore&lt;/li&gt;
&lt;li&gt;Artistic filters&lt;/li&gt;
&lt;li&gt;AI image generation&lt;/li&gt;
&lt;li&gt;Optimized media exports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than demonstrating a single Cloudinary transformation, CloudCanvas combines several capabilities into one complete creative workflow.&lt;/p&gt;

&lt;p&gt;It was also the first submission we received, setting a strong pace for the rest of the challenge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3azz401xta1t9kfcs75l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3azz401xta1t9kfcs75l.png" alt="CloudCanvas" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the project:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloud-canvas-fr.vercel.app/" rel="noopener noreferrer"&gt;Try the live demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Snap Optimizer AI, by Aashi
&lt;/h2&gt;

&lt;p&gt;Some projects solve one media problem extremely well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snap Optimizer AI&lt;/strong&gt; solves several.&lt;/p&gt;

&lt;p&gt;Aashi built an all-in-one image optimization platform that brings enhancement, editing, compression, resizing, and asset creation into one interface.&lt;/p&gt;

&lt;p&gt;Users can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enhance image quality using AI&lt;/li&gt;
&lt;li&gt;Remove backgrounds&lt;/li&gt;
&lt;li&gt;Compress large image files&lt;/li&gt;
&lt;li&gt;Resize and crop images intelligently&lt;/li&gt;
&lt;li&gt;Generate passport photos&lt;/li&gt;
&lt;li&gt;Create profile pictures&lt;/li&gt;
&lt;li&gt;Prepare assets for Instagram, YouTube, and LinkedIn&lt;/li&gt;
&lt;li&gt;Enter prompts for custom transformations&lt;/li&gt;
&lt;li&gt;Compare images before and after optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The platform was built with Python, Flask, Cloudinary AI, and the &lt;code&gt;cloudinary-transformations&lt;/code&gt; skill.&lt;/p&gt;

&lt;p&gt;The skill allows users to describe an intended result in natural language, then helps convert that request into a real Cloudinary transformation workflow.&lt;/p&gt;

&lt;p&gt;Snap Optimizer AI stood out because it was not just a demonstration. It felt like a practical tool that creators, marketers, developers, and everyday users could immediately understand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flxjr4b0sefe06rkxz2yk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flxjr4b0sefe06rkxz2yk.png" alt="Snap Optimizer AI" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the project:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/aashi-1926/snap-optimizer" rel="noopener noreferrer"&gt;View the GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://snap-optimizer.onrender.com/" rel="noopener noreferrer"&gt;Try the live demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Eirene, by Nivrithi
&lt;/h2&gt;

&lt;p&gt;While the other submissions focused primarily on media creation and developer productivity, &lt;strong&gt;Eirene&lt;/strong&gt; applied the technology to a deeply human problem.&lt;/p&gt;

&lt;p&gt;Eirene is a postpartum recovery companion built around an often-overlooked reality: while the arrival of a baby is widely celebrated, the mother’s physical and emotional recovery can receive far less attention.&lt;/p&gt;

&lt;p&gt;The platform helps mothers document and understand their recovery through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Week-by-week recovery tracking&lt;/li&gt;
&lt;li&gt;Mood tracking&lt;/li&gt;
&lt;li&gt;Daily reflections&lt;/li&gt;
&lt;li&gt;Personalized affirmations&lt;/li&gt;
&lt;li&gt;Recovery milestones&lt;/li&gt;
&lt;li&gt;A visual recovery timeline&lt;/li&gt;
&lt;li&gt;Photo memories with calming effects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloudinary powers the image upload experience and visual effects, helping users preserve recovery memories inside the journal.&lt;/p&gt;

&lt;p&gt;The project was built using HTML, CSS, JavaScript, and the Cloudinary transformations skill.&lt;/p&gt;

&lt;p&gt;Eirene showed that media tools do not need to be limited to marketing, design, or developer workflows. They can also support reflection, memory, wellbeing, and personal storytelling.&lt;/p&gt;

&lt;p&gt;The community response reflected that. As Jen Looper shared during the showcase, this project particularly resonated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcqwwnlc77nt937rrx9ke.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcqwwnlc77nt937rrx9ke.png" alt="Eirene" width="799" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the project:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://eirene-journal.vercel.app/" rel="noopener noreferrer"&gt;Try the live demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CreatorPilot AI, by Ved Prakash Arya
&lt;/h2&gt;

&lt;p&gt;Ved Prakash Arya, founder of Snapforest, built &lt;strong&gt;CreatorPilot AI&lt;/strong&gt; after repeatedly running into the same problem.&lt;/p&gt;

&lt;p&gt;Learning and experimenting with Cloudinary often meant switching between documentation, transformation generators, SDK examples, media management tools, and several browser tabs.&lt;/p&gt;

&lt;p&gt;CreatorPilot AI brings those experiences into one workspace.&lt;/p&gt;

&lt;p&gt;The platform includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An AI Documentation Assistant&lt;/li&gt;
&lt;li&gt;A Transformation Studio&lt;/li&gt;
&lt;li&gt;A React SDK Playground&lt;/li&gt;
&lt;li&gt;A Media Manager&lt;/li&gt;
&lt;li&gt;An AI Image Generator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It supports workflows such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Background removal&lt;/li&gt;
&lt;li&gt;Automatic image enhancement&lt;/li&gt;
&lt;li&gt;Image upscaling&lt;/li&gt;
&lt;li&gt;Generative fill&lt;/li&gt;
&lt;li&gt;Background replacement&lt;/li&gt;
&lt;li&gt;Object removal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CreatorPilot AI was the only submission to use all three skills from the Cloudinary Skills Pack.&lt;/p&gt;

&lt;p&gt;It was built with React 19, TypeScript, Vite, Tailwind CSS, &lt;code&gt;@cloudinary/react&lt;/code&gt;, and &lt;code&gt;@cloudinary/url-gen&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result is part learning environment, part experimentation platform, and part developer productivity tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3yv2a85bwo4ju7cpi4as.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3yv2a85bwo4ju7cpi4as.png" alt="CreatorPilot AI" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the project:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://creatorpilot-ai-eosin.vercel.app/" rel="noopener noreferrer"&gt;Try the live demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vedparkasharya/creatorpilot-ai" rel="noopener noreferrer"&gt;View the GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Brandify, by Princess Bube
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Brandify&lt;/strong&gt; started with a problem Princess had encountered during her consulting work.&lt;/p&gt;

&lt;p&gt;Maintaining a consistent visual identity across dozens of images, formats, campaigns, and platforms is difficult, especially when every business has its own branding rules.&lt;/p&gt;

&lt;p&gt;Brandify allows a user to upload a brand style guide alongside a collection of images. The platform then generates asset variations that follow the brand’s documented colors, visual direction, and presentation requirements.&lt;/p&gt;

&lt;p&gt;Claude reads and interprets the brand guide, while Cloudinary generates the required image derivatives.&lt;/p&gt;

&lt;p&gt;Brandify also includes a second workflow for technical content.&lt;/p&gt;

&lt;p&gt;Users can upload a draft blog post, and the application scans the Cloudinary URLs embedded inside it. It identifies missing optimizations and rewrites the URLs using Cloudinary delivery best practices.&lt;/p&gt;

&lt;p&gt;That combination makes Brandify both a creative branding assistant and a media optimization tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37cdlm6o9cai04co6lri.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37cdlm6o9cai04co6lri.png" alt="Brandify" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the project:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Princesso/brandify" rel="noopener noreferrer"&gt;View the GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Taking the Projects Live
&lt;/h2&gt;

&lt;p&gt;On July 10, our creators presented their projects during a live judging session streamed across LinkedIn and YouTube.&lt;/p&gt;

&lt;p&gt;This was not just a slideshow of finished products.&lt;/p&gt;

&lt;p&gt;Each creator walked through the problem they had chosen, how they approached it, where the Skills Pack helped, and what they had managed to build within the challenge window.&lt;/p&gt;

&lt;p&gt;The judges evaluated every project using four criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Creativity&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Practical usefulness&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effective use of the Cloudinary Skills Pack&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Overall execution&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The live format gave the broader community an opportunity to watch the demos, ask questions, celebrate the builders, and see what the Skills Pack could enable beyond a standard tutorial.&lt;/p&gt;

&lt;p&gt;Missed the session?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/feed/update/urn:li:ugcPost:7480196337502793729" rel="noopener noreferrer"&gt;Watch the LinkedIn recording&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=G5GO306sD0c" rel="noopener noreferrer"&gt;Watch the YouTube recording&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  And the Winner Is…
&lt;/h2&gt;

&lt;p&gt;For our previous Earth Month Mini-Hack, we celebrated every valid submission without selecting a single winner.&lt;/p&gt;

&lt;p&gt;This time, we chose one.&lt;/p&gt;

&lt;p&gt;Congratulations to &lt;strong&gt;Aashi&lt;/strong&gt;, winner of the Cloudinary Creators June Mini-Hack for &lt;strong&gt;Snap Optimizer AI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The project stood out for combining an impressive range of genuinely useful features into one polished and accessible experience.&lt;/p&gt;

&lt;p&gt;It covered enhancement, compression, background removal, intelligent resizing, social media asset creation, passport photo generation, prompt-based transformations, and before-and-after comparisons.&lt;/p&gt;

&lt;p&gt;Most importantly, the project demonstrated how the Cloudinary Transformations skill could move beyond a developer experiment and become part of an experience that real users could immediately understand.&lt;/p&gt;

&lt;p&gt;Aashi will receive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloudinary swag&lt;/li&gt;
&lt;li&gt;AI credits&lt;/li&gt;
&lt;li&gt;A featured place in the Community Showcase&lt;/li&gt;
&lt;li&gt;Promotion across Cloudinary social channels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Congratulations on an incredibly well-executed project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Mini-Hack Proved
&lt;/h2&gt;

&lt;p&gt;Our Earth Month challenge showed what happens when you give developers a narrow problem and a clear constraint.&lt;/p&gt;

&lt;p&gt;The June Mini-Hack showed what happens when you do the opposite.&lt;/p&gt;

&lt;p&gt;Give creators a capable toolkit and enough freedom, and they do not necessarily scatter. They build around the problems they already understand.&lt;/p&gt;

&lt;p&gt;A consulting challenge became a branding assistant.&lt;/p&gt;

&lt;p&gt;Too many documentation tabs became a developer copilot.&lt;/p&gt;

&lt;p&gt;An interest in creative editing became a browser-based media studio.&lt;/p&gt;

&lt;p&gt;A need for simpler optimization became an all-in-one image platform.&lt;/p&gt;

&lt;p&gt;A deeply personal healthcare gap became a recovery journal.&lt;/p&gt;

&lt;p&gt;Five creators started with the same Skills Pack and arrived at five completely different products.&lt;/p&gt;

&lt;p&gt;That range is not a side effect of the challenge.&lt;/p&gt;

&lt;p&gt;It is the strongest result.&lt;/p&gt;

&lt;p&gt;The best way to understand what a new developer tool can enable is often to release it into the hands of builders, step back, and see where they take it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;You probably know how this section ends.&lt;/p&gt;

&lt;p&gt;We are doing it again.&lt;/p&gt;

&lt;p&gt;The next Cloudinary Creators challenge is already taking shape, and we will share the full details with the community soon.&lt;/p&gt;

&lt;p&gt;Until then, explore the projects, watch the judging session, and start thinking about what you might build when the next challenge opens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stay Connected With the Cloudinary Creators Community
&lt;/h2&gt;

&lt;p&gt;Applications for the current Cloudinary Creators cohort are now closed, and our focus is on supporting the creators already participating in the program.&lt;/p&gt;

&lt;p&gt;You can still follow what the community is building, explore the projects from this Mini-Hack, and stay updated on future challenges and opportunities.&lt;/p&gt;

&lt;p&gt;Learn more about the Cloudinary Creators program:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloudinary.com/developers/community" rel="noopener noreferrer"&gt;https://cloudinary.com/developers/community&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Join our Discord community to meet other builders, get project feedback, and hear about upcoming Mini-Hacks:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://discord.com/invite/D8ddQj6KnH" rel="noopener noreferrer"&gt;https://discord.com/invite/D8ddQj6KnH&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Work through the Cloudinary Creators curriculum:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://training.cloudinary.com/pages/c2c" rel="noopener noreferrer"&gt;https://training.cloudinary.com/pages/c2c&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Explore the Cloudinary Skills Pack:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/cloudinary-devs/skills" rel="noopener noreferrer"&gt;https://github.com/cloudinary-devs/skills&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next challenge has not started yet.&lt;/p&gt;

&lt;p&gt;Your next project can.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Cloudinary ❤️ developers&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👉 &lt;strong&gt;&lt;a href="https://link.cloudinary.com/uqnvw" rel="noopener noreferrer"&gt;Create your free account&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>cloudinary</category>
      <category>community</category>
      <category>hackathon</category>
      <category>minihack</category>
    </item>
    <item>
      <title>Building a World Cup card battler that turns your selfie into a football card (Next.js, Cloudinary, Supabase, Vercel)</title>
      <dc:creator>Nick Bradley</dc:creator>
      <pubDate>Tue, 14 Jul 2026 21:44:56 +0000</pubDate>
      <link>https://dev.to/cloudinary/building-a-world-cup-card-battler-that-turns-your-selfie-into-a-football-card-nextjs-cloudinary-4i88</link>
      <guid>https://dev.to/cloudinary/building-a-world-cup-card-battler-that-turns-your-selfie-into-a-football-card-nextjs-cloudinary-4i88</guid>
      <description>&lt;p&gt;The World Cup is on, half my feed is football, and I wanted an excuse to build something. So I built &lt;strong&gt;Kickoff Cards&lt;/strong&gt;: upload a selfie, pick your nation, and you become a playable football trading card that you battle other people's cards with to earn XP and climb a live leaderboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The game
&lt;/h2&gt;

&lt;p&gt;The idea is simple: you turn yourself into a football trading card. Think of a Panini sticker or a FIFA Ultimate Team card, except the player on it is you.&lt;/p&gt;

&lt;p&gt;You upload a selfie and pick a nation, and a few seconds later you get a card back: your face cut out and dropped into the frame in your nation's colours, with six stats down the side (pace, shooting, passing, and so on) that decide how it performs. There's no signup to get this far; you get an anonymous session on first load and the card belongs to you. Anything you upload is run through automated moderation before it appears in the public gallery.&lt;/p&gt;

&lt;p&gt;Then you play it. You send your card into the battle arena, it's matched against another player's, and the two go head to head across those six stats. Winning earns XP, enough XP levels the card up, and a level-up nudges its stats and regenerates the card image to match. A leaderboard tracks who's ahead and updates in real time as battles resolve.&lt;/p&gt;

&lt;p&gt;That's the whole loop: make a card, fight with it, level it up, climb the board. It's small, but it touches most of what a real user-media app has to deal with: uploads, AI image processing, moderation, live state, and public content.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkx11vc6mtb2zmxnx707h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkx11vc6mtb2zmxnx707h.png" alt="Kickoff cards battle" width="800" height="679"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The card game is the demo. What this post really focuses on is how the four tools it's built on fit together.&lt;/p&gt;

&lt;p&gt;I deliberately chose tooling that works well with agentic development environments: current, trustworthy docs and MCP servers that let the agent check its work against the real project rather than guess from training data. Next.js, Cloudinary, Supabase and Vercel are leaders in this field, and that's a fair part of why this came together quickly. More on that later.&lt;/p&gt;

&lt;p&gt;The split is clean: Next.js owns the app, Cloudinary owns media, Supabase owns data, and Vercel ships it, each handing off to the others through stable APIs. It's open source, runs entirely on free tiers. You can play it at &lt;strong&gt;&lt;a href="https://kickoff-cards.vercel.app" rel="noopener noreferrer"&gt;kickoff-cards.vercel.app&lt;/a&gt;&lt;/strong&gt;, and the code for you to fork and run yourself: &lt;strong&gt;&lt;a href="https://github.com/cloudinary-devs/kickoff-cards" rel="noopener noreferrer"&gt;github.com/cloudinary-devs/kickoff-cards&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the whole flow on one diagram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
  │
  ├─ GET page ──→ Next.js Server Component ──→ Supabase (RLS)
  │                                        └─→ Cloudinary URL builder
  │
  ├─ POST /api/sign-upload ──→ signed upload params
  │
  └─ Direct upload ─────────→ Cloudinary
                                  │
                          Async moderation
                                  │
                              Webhook ──→ Supabase update ──→ Realtime ──→ Browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of this post walks through the decisions behind that diagram.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not just store the files?
&lt;/h2&gt;

&lt;p&gt;The first decision was what to do with the uploaded photos. The easy version is: save the file, serve it back, done. That holds up until you actually look at what a card needs: background removal, a face-aware crop, compositing onto the template, text overlays, modern formats, and moderation on anything a stranger uploads. Do it yourself and you're suddenly maintaining a transform pipeline, a CDN, and a moderation step.&lt;/p&gt;

&lt;p&gt;I didn't want to build that, so the image work goes to Cloudinary: I describe the transformation I want, get a URL back, and it generates and caches the result on first request. The useful side effect is that a finished card ends up being a single URL with no server-side rendering, which I'll come back to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Uploads that never touch my server
&lt;/h2&gt;

&lt;p&gt;A lot of upload flows go browser → your server → storage, which means your server parses multipart bodies, buffers files, and streams them back out. Kickoff Cards skips that: the server signs a set of upload parameters, hands them to the browser, and the browser uploads straight to Cloudinary. The only thing my code produces is a signature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSignedUploadParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publicId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;avatar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`kickoff/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/avatar`&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`kickoff/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/uploads/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cloudinary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;api_sign_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;public_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;publicId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;overwrite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;moderation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notification_url&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLOUDINARY_API_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cloudName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the signature covers every parameter, the client can't change the destination path, the moderation settings, the webhook URL, or the overwrite behaviour without invalidating it. You get direct uploads while the server keeps control of where files land.&lt;/p&gt;

&lt;p&gt;This is deliberately not an upload preset. Presets work for static config, but they aren't user-aware: they can't carry the authenticated user's ID, a per-request destination, or a dynamic webhook. That gets generated server-side and signed; the browser submits it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sessions without signup
&lt;/h2&gt;

&lt;p&gt;That &lt;code&gt;userId&lt;/code&gt; in the upload path comes from Supabase. There's no login: Supabase Anonymous Auth gives each visitor a real authenticated session on first load, backed by a cookie and a stable user ID, and their cards, battles, and XP all hang off that ID. Uploads are scoped to it (&lt;code&gt;kickoff/users/{userId}/...&lt;/code&gt;), so a card belongs to the right person without any extra plumbing.&lt;/p&gt;

&lt;p&gt;It's a reasonable fit for a low-friction demo, and it isn't a dead end: if I later add OAuth or magic links, anonymous is just another provider and the data model doesn't change.&lt;/p&gt;




&lt;h2&gt;
  
  
  A whole trading card is one URL
&lt;/h2&gt;

&lt;p&gt;The finished card (background removed, face cropped into the window, six stats printed at set coordinates, watermark on the bottom) isn't rendered on a server. No canvas, no image endpoint, nothing written to disk. It's a single Cloudinary URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transformation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;layerId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;background_removal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;crop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fill&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gravity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;face&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tmpl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tmpl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;layer_apply&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gravity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tmpl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gravity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tmpl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tmpl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="c1"&gt;// six stat overlays, one per attribute&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;STAT_ATTRS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;font_family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Arial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;font_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;font_weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;black&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;layer_apply&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gravity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;north&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]),&lt;/span&gt;

  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kickoff:cloudinary-logo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;crop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;55&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;layer_apply&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gravity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;south&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;crop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;limit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cloudinary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`templates/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nation&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;transformation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fetch_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloudinary runs that chain the first time the URL is requested, caches the result, and serves subsequent requests from the CDN. There's no image-processing server to run.&lt;/p&gt;

&lt;p&gt;A few things to watch for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Order matters.&lt;/strong&gt; Background removal has to come before the crop. Flip them and Cloudinary crops the original first, then removes the background from that rectangle, which gives a worse result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gravity: "face"&lt;/code&gt; matters.&lt;/strong&gt; Without it, a full-body selfie tends to crop to the chest rather than the face.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer IDs use colons, not slashes.&lt;/strong&gt; &lt;code&gt;kickoff/users/abc/uploads/xyz&lt;/code&gt; becomes &lt;code&gt;kickoff:users:abc:uploads:xyz&lt;/code&gt;. Small detail, easy to miss when debugging transformation URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache busting is automatic.&lt;/strong&gt; When a player levels up, I rebuild the URL with the new stats. A different URL is a different cache key, so Cloudinary renders the updated card on the next request, with no purge calls or CDN management.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every browser-facing image ends with &lt;code&gt;fetch_format: "auto"&lt;/code&gt; and &lt;code&gt;quality: "auto"&lt;/code&gt;, so Cloudinary serves WebP/AVIF where supported and picks a compression level, usually smaller files with no extra config.&lt;/p&gt;

&lt;p&gt;One deliberate non-choice: I'm not using &lt;code&gt;next-cloudinary&lt;/code&gt;. The card URLs are already complete transformation URLs stored in the database. Plain &lt;code&gt;next/image&lt;/code&gt; with &lt;code&gt;unoptimized&lt;/code&gt; handles layout, sizing, and lazy-loading, while Cloudinary stays responsible for the media. That's one fewer abstraction between the app and the transformation pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Moderation on upload
&lt;/h2&gt;

&lt;p&gt;If you accept user images, you need moderation, because some fraction of "selfies" won't be selfies. The DIY version is a vision API plus a queue, retries, and another set of credentials. Cloudinary handles it as part of the upload instead.&lt;/p&gt;

&lt;p&gt;Every signed upload carries an AWS Rekognition moderation string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;aws_rek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;explicit_nudity:0.4:suggestive:0.4:violence:0.4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the upload, Cloudinary runs moderation asynchronously and POSTs the verdict to my webhook, which verifies the signature, reads the status, updates the row in Supabase, and deletes anything rejected. The user watches their card flip from &lt;strong&gt;Pending&lt;/strong&gt; to &lt;strong&gt;Approved&lt;/strong&gt; (or get bounced) without refreshing, because that database write feeds a Realtime subscription. That's where Supabase comes in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Supabase: the right key for the right job
&lt;/h2&gt;

&lt;p&gt;I use three Supabase clients on purpose, and keeping them separate avoids a common class of security bug.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lib/supabase/server.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anon&lt;/td&gt;
&lt;td&gt;Server Components, Route Handlers, Server Actions (RLS enforced)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lib/supabase/client.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anon&lt;/td&gt;
&lt;td&gt;Browser Realtime subscriptions only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lib/supabase/admin.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Service role&lt;/td&gt;
&lt;td&gt;Webhooks and privileged server work (bypasses RLS)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The service-role key bypasses Row Level Security, so it lives in one &lt;code&gt;server-only&lt;/code&gt; module. If it's accidentally imported into client code, the build fails rather than leaking the key. Two places use it intentionally: the moderation webhook you just saw (which has to delete any card) and battle resolution (the &lt;code&gt;battles&lt;/code&gt; table has &lt;code&gt;WITH CHECK (false)&lt;/code&gt; on insert, so results can't be written from the client). Everything else runs under normal RLS, which is easier to reason about than one client that's sometimes an admin and sometimes not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Live updates without polling
&lt;/h3&gt;

&lt;p&gt;Two bits of UI update live: the leaderboard and moderation status. Both use the same pattern: subscribe to a table and let Postgres changes push to the client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;leaderboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgres_changes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;profiles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a battle changes someone's XP, the leaderboard re-renders. When Cloudinary's webhook updates a card, the card page reacts. No polling and no WebSocket server to run yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;External service → Webhook → Postgres → Supabase Realtime → Browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same shape works for other async work: payments, AI jobs, background processing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building this with an agent
&lt;/h2&gt;

&lt;p&gt;Here's the "more on that" from the intro. The reason the stack choice mattered is that all three of Cloudinary, Supabase and Vercel expose MCP servers, so the agent can query the actual project rather than rely on training data. It can browse uploaded assets and test a transformation in Cloudinary, check the live schema and RLS policies in Supabase, and read deployment logs in Vercel. For a media-heavy app that's the difference between the agent guessing and the agent checking: instead of hardcoding a URL and refreshing a tab to see if a transformation worked, it verifies the result directly.&lt;/p&gt;

&lt;p&gt;The repo supports this with an &lt;code&gt;AGENTS.md&lt;/code&gt; (architecture rules, conventions, and which dependencies move fast enough that it should fetch current docs) and vendored skills under &lt;code&gt;.agents/skills/&lt;/code&gt; for Supabase and Postgres, so it works from current guidance when writing migrations.&lt;/p&gt;

&lt;p&gt;Nothing here is agent-specific, really. The same things that make the codebase easy for an agent to work in (clear boundaries, one place per concern, services it can query directly) are what make it easy for a person. The agent just makes it obvious when you've got those wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/cloudinary-devs/kickoff-cards
&lt;span class="nb"&gt;cd &lt;/span&gt;kickoff-cards
pnpm &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.example .env.local
pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll need a free Cloudinary account with the AWS Rekognition add-on enabled (the free quota is plenty), a Supabase project with anonymous auth on, and the schema from &lt;code&gt;supabase/schema.sql&lt;/code&gt;. For local webhook testing, a Cloudflare tunnel does the job without an account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cloudflared tunnel &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:3000
&lt;span class="c"&gt;# paste the HTTPS URL into CLOUDINARY_WEBHOOK_URL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full walkthrough is in &lt;code&gt;docs/getting-started.md&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What carries over
&lt;/h2&gt;

&lt;p&gt;Aside from the football, these are the patterns that apply to most apps handling user media:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signed direct uploads.&lt;/strong&gt; The browser uploads to Cloudinary, the server only signs. The file never passes through your app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One transformation builder.&lt;/strong&gt; Keep the transform logic in a single function rather than scattered across components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate Supabase clients.&lt;/strong&gt; Anon, browser, and service-role as distinct responsibilities, so RLS isn't bypassed by accident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhook + Realtime.&lt;/strong&gt; External service → webhook → database → Realtime → browser. No polling, and it generalises to moderation, payments, and background jobs.&lt;/p&gt;

&lt;p&gt;None of these are specific to a card game, or to this stack. They're just what fell out of giving each service one clear job and letting it do it.&lt;/p&gt;

&lt;p&gt;If you want to poke at the code, most of it lives in &lt;code&gt;lib/cloudinary/&lt;/code&gt; and &lt;code&gt;lib/supabase/&lt;/code&gt;. It's MIT-licensed, so take what's useful.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Cloudinary ❤️ developers&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👉 &lt;strong&gt;&lt;a href="https://link.cloudinary.com/uryzi" rel="noopener noreferrer"&gt;Create your free account&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>cloudinary</category>
      <category>nextjs</category>
      <category>supabase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Cloudinary's New Image Generation API: One API, Multiple AI Models, and Built-in Asset Management</title>
      <dc:creator>Jen Looper</dc:creator>
      <pubDate>Wed, 08 Jul 2026 13:02:03 +0000</pubDate>
      <link>https://dev.to/cloudinary/cloudinarys-new-image-generation-api-one-api-multiple-ai-models-and-built-in-asset-management-3ldl</link>
      <guid>https://dev.to/cloudinary/cloudinarys-new-image-generation-api-one-api-multiple-ai-models-and-built-in-asset-management-3ldl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;At conference booths, developers often ask whether we support image generation at Cloudinary, given our emphasis on media management. As of now, I can say "YES! yes, we do" - here's how!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cloudinary's Image Generation API lets developers generate images from text prompts using multiple AI model families, then store the result as a managed Cloudinary asset for delivery, optimization, resizing, and transformation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Original image copyright © REUTERS/ABC Affiliate WABC&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this tutorial, we'll use Python to call the API, generate an image, and save the final result to Cloudinary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll build
&lt;/h2&gt;

&lt;p&gt;A Python script that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate an image from a text prompt&lt;/li&gt;
&lt;li&gt;Choose a model family like &lt;code&gt;flux&lt;/code&gt;, &lt;code&gt;recraft&lt;/code&gt;, &lt;code&gt;gpt-image&lt;/code&gt;, &lt;code&gt;ideogram&lt;/code&gt;, or &lt;code&gt;nano-banana&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Save generated output as a managed Cloudinary asset&lt;/li&gt;
&lt;li&gt;Print the final image URL, public ID, size, and file size&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;First, follow &lt;a href="https://cloudinary.com/documentation/image_generation_addon" rel="noopener noreferrer"&gt;the instructions on Cloudinary docs&lt;/a&gt; to install the image generation add-on to your account. While you're in the Cloudinary console, make note of your API key, secret and the cloud name where you want assets to be stored.&lt;/p&gt;

&lt;p&gt;Install the Python dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Export your Cloudinary credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLOUDINARY_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-api-key"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLOUDINARY_API_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-api-secret"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLOUDINARY_CLOUD_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-cloud-name"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's your &lt;a href="https://gist.github.com/jlooper/4ef0ac33a6bd6e943d953f523d606412" rel="noopener noreferrer"&gt;script&lt;/a&gt;. Save it as 'generate.py'.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate your first AI image
&lt;/h2&gt;

&lt;p&gt;Run the script with a prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 generate.py &lt;span class="s2"&gt;"A medieval monk"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Farplam1tplas8pfkhdaf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Farplam1tplas8pfkhdaf.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This image was generated with the default Flux model&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The script sends a request to Cloudinary's Image Generation API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;family&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model_family&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;managed_asset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;public_id&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it calls the API using HTTP Basic Auth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;IMAGE_GEN_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/generate/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cloud_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/text_to_image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLOUDINARY_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLOUDINARY_API_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Choose an AI image model
&lt;/h2&gt;

&lt;p&gt;This is the fun part. You can pick the image generation model that best suits your use case. For example, I find that nano-banana works well with images that include text.&lt;/p&gt;

&lt;p&gt;You can switch model families without changing the rest of your application code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 generate.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"A futuristic Tokyo skyline at sunset"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; flux &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tier&lt;/span&gt; premium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported model families include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flux
gpt-image
ideogram
recraft
nano-banana
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe9vz84u8iq56c3vs7lfv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe9vz84u8iq56c3vs7lfv.png" alt="Another monk" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This was the same prompt, done with Ideogram model&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That makes it easier to test different visual styles while keeping one integration path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save generated images as Cloudinary assets
&lt;/h2&gt;

&lt;p&gt;The important part of the request is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;managed_asset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;public_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Cloudinary to save the generated image as a managed asset on your account, instead of returning only a temporary output.&lt;/p&gt;

&lt;p&gt;After generation, the script prints something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Image ready!
Public ID: generated/fox-hiking
URL: https://res.cloudinary.com/...
Size: 1024×1024 px
File size: 840 KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the image is in Cloudinary, you can resize it, optimize it, crop it, transform it, and deliver it through Cloudinary's CDN. Wicked fast and easy!&lt;/p&gt;

&lt;h2&gt;
  
  
  Full usage examples
&lt;/h2&gt;

&lt;p&gt;Generate a new image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 generate.py &lt;span class="s2"&gt;"Marie de France playing pool"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuv5iwudlfmr3567cibqm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuv5iwudlfmr3567cibqm.png" alt="Marie de France" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I love this image! Marie de France was a 12th century author of some famous french literature that I particularly like. I don't think she ever played pool, though. Also this pool table is awesomely 'AI-pilled"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Use a premium model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 generate.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"A cinematic product photo of a sneaker floating over water"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; flux &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tier&lt;/span&gt; premium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this is useful
&lt;/h2&gt;

&lt;p&gt;Most image generation APIs return an output.&lt;/p&gt;

&lt;p&gt;Cloudinary's Image Generation API returns an output that can immediately become part of your media pipeline.&lt;/p&gt;

&lt;p&gt;That means developers can generate an image and then use the same platform to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store it&lt;/li&gt;
&lt;li&gt;Transform it&lt;/li&gt;
&lt;li&gt;Optimize it&lt;/li&gt;
&lt;li&gt;Resize it&lt;/li&gt;
&lt;li&gt;Deliver it&lt;/li&gt;
&lt;li&gt;Reuse it across applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For apps that already manage media with Cloudinary, AI image generation becomes part of the existing workflow instead of a separate one-off process. The pipeline just got way simplified for you!&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;This Python script is small, but it covers the core production workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authenticate with Cloudinary&lt;/li&gt;
&lt;li&gt;Send a prompt to the Image Generation API&lt;/li&gt;
&lt;li&gt;Choose a model family and tier&lt;/li&gt;
&lt;li&gt;Save the result as a managed asset&lt;/li&gt;
&lt;li&gt;Return a usable image URL&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building developer tools, e-commerce workflows, campaign generators, or AI-powered creative apps, this approach gives you both image generation and image delivery in the same pipeline.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Cloudinary ❤️ developers&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👉 &lt;strong&gt;&lt;a href="https://link.cloudinary.com/urmty" rel="noopener noreferrer"&gt;Create your free account&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>ai</category>
      <category>nanobanana</category>
      <category>codenewbie</category>
      <category>images</category>
    </item>
    <item>
      <title>Your model returns a file. Production needs an asset.</title>
      <dc:creator>Nick Bradley</dc:creator>
      <pubDate>Mon, 06 Jul 2026 13:40:43 +0000</pubDate>
      <link>https://dev.to/cloudinary/your-model-returns-a-file-production-needs-an-asset-17ac</link>
      <guid>https://dev.to/cloudinary/your-model-returns-a-file-production-needs-an-asset-17ac</guid>
      <description>&lt;p&gt;Your agent calls an AI image or video model (OpenAI, Google, Runway, fal) and, a few seconds later, gets back a URL or a blob of base64. Feels like the job is done. It isn't. That raw output will probably expire, it's the wrong size and format for wherever it's headed, it carries no record of what made it, and nobody has checked it against your brand. And because it's an agent, there's no human standing by to fix any of that.&lt;/p&gt;

&lt;p&gt;The disappearing URL gets all the attention, but it's only the first of several steps between "the model responded" and "I can ship this." Here's the whole path.&lt;/p&gt;

&lt;p&gt;What this covers: what happens when generated media expires across the major APIs, what "production-ready" actually requires (persist, transform, deliver, brand, find), and how to collapse that pipeline into one step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step zero: the output is built to disappear
&lt;/h2&gt;

&lt;p&gt;Generated media is ephemeral by default, and the details vary more than you'd expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI:&lt;/strong&gt; image models like &lt;code&gt;gpt-image-2&lt;/code&gt; return base64 bytes, no hosted URL at all, so you own storage from the first response. Sora video files last ~48 hours (download URLs ~1 hour), and the Sora video &lt;strong&gt;API is being sunset in September 2026&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google:&lt;/strong&gt; the current image models, &lt;strong&gt;Nano Banana 2 (Gemini 3.1 Flash Image)&lt;/strong&gt; and &lt;strong&gt;Nano Banana Pro (Gemini 3 Pro Image)&lt;/strong&gt;, return bytes inline through the Gemini API. For video, &lt;strong&gt;Veo 3.1&lt;/strong&gt; output is kept &lt;strong&gt;two days&lt;/strong&gt; behind the Files API, then deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runway:&lt;/strong&gt; Gen-4 image and video output URLs expire in roughly &lt;strong&gt;24–48 hours&lt;/strong&gt;; the docs tell you plainly to download and self-host.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a bug. These are inference platforms, not asset hosts, and serving files forever is expensive. The trap is that "save it before it vanishes" quietly became &lt;em&gt;your&lt;/em&gt; job, and most teams only find out when a link 404s in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Persistence alone isn't the finish line
&lt;/h3&gt;

&lt;p&gt;The landscape is shifting, to be fair. &lt;strong&gt;fal.ai now offers fal Assets&lt;/strong&gt;, where generations persist instead of expiring, and platforms like Leonardo keep outputs indefinitely. Even routing layers like &lt;strong&gt;OpenRouter's Unified Image API&lt;/strong&gt; give you one endpoint across dozens of models, though as a router it stores nothing, so persistence is still yours to solve.&lt;/p&gt;

&lt;p&gt;But solving expiry only solves step zero. A file that lives forever in one vendor's library is still unoptimised, unbranded, scattered across whichever providers you happen to use, and only as findable as that vendor's UI. The hard part was never just &lt;em&gt;keeping&lt;/em&gt; the bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why autonomous agents make media persistence harder
&lt;/h2&gt;

&lt;p&gt;When a person generates an image in a web UI, they're right there to download it, crop it, eyeball the brand, and drop it where it belongs. Agentic workflows delete that entire safety net.&lt;/p&gt;

&lt;p&gt;An agent runs in the background. It calls a model, gets a result, and moves on: the output lands in a state object or a database row, and nobody looks until much later. Good agent-state design even says to store large artifacts as files and keep only the path in state. Sound advice, but it assumes the file is somewhere permanent &lt;em&gt;and&lt;/em&gt; usable. A raw model URL is neither. So every step a human would have done by hand now has to happen automatically, at generation time. That's the actual job.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "production-ready" actually means
&lt;/h2&gt;

&lt;p&gt;Strip it back to what has to be true before generated media can ship:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Persisted:&lt;/strong&gt; captured the instant it's created, so nothing depends on an expiring URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimised &amp;amp; transformed:&lt;/strong&gt; the right format and size for each destination. A 4 MB PNG becomes a 40 KB WebP thumbnail, a social crop, an AVIF for modern browsers. Video is harder: transcoding to MP4/WebM, an adaptive HLS/DASH stream, a poster frame, often a trimmed cut, all from one master.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivered:&lt;/strong&gt; served fast from a CDN, video streamed adaptively rather than shipped whole from one region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-brand:&lt;/strong&gt; checked against brand rules before a customer sees it. An agent generating customer-facing media with no brand guardrail is a real risk, not a hypothetical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Findable:&lt;/strong&gt; stored with its metadata: prompt, model, use-case, tags, and for video things like duration and captions. An agent that can't search its own output regenerates the same thing twice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Miss any one of these and you don't have an asset; you have a raw output and a to-do list.&lt;/p&gt;

&lt;h3&gt;
  
  
  This is really an asset pipeline
&lt;/h3&gt;

&lt;p&gt;Every one of those requirements already exists in traditional media workflows. AI generation hasn't removed the pipeline, it has simply automated the first step. Agents make that more obvious because there's no human left to bridge the gap manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the media pipeline yourself
&lt;/h2&gt;

&lt;p&gt;The pattern everyone reaches for looks simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;generate → download the bytes → push to your object store → serve from there&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The store is usually S3, Cloudflare R2, or Supabase, and the mechanics are well-trodden. But persisting the bytes is the easy ~60%. What's left on your plate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Glue code on every generation path&lt;/strong&gt;, with retries for when the URL expires mid-download (worse for large video files).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A transformation service&lt;/strong&gt; (Lambda + sharp, imgix) and, for video, &lt;strong&gt;a transcoding pipeline&lt;/strong&gt; (FFmpeg or managed) for streamable renditions and poster frames.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A CDN&lt;/strong&gt; in front, with adaptive streaming for video.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A brand check&lt;/strong&gt;, which most pipelines just skip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A metadata store&lt;/strong&gt; you build and maintain so assets stay findable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can absolutely assemble this, and if storage cost dominates and your transform needs are light, R2-plus-a-Worker is a sensible call. Just be honest that you're now operating a small media pipeline, not a bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two pipelines, one call
&lt;/h2&gt;

&lt;p&gt;The model step is identical either way. What differs is everything &lt;em&gt;after&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu0si4mowxea0fr4cz2ja.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu0si4mowxea0fr4cz2ja.png" alt="Two pipelines: the DIY chain of services versus a single managed step" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The generation call below is deliberately abstracted: whether &lt;code&gt;generateMedia()&lt;/code&gt; wraps OpenAI, Google, Runway, fal, or an OpenRouter route, you're left holding the same thing: an ephemeral URL you now have to productionise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The DIY way:&lt;/strong&gt; download, race the expiry, persist, then start the rest of the pipeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;S3Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PutObjectCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-s3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;S3Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AWS_REGION&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelOutputUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// any provider; ephemeral&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelOutputUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// race the clock&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Model URL expired: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutObjectCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S3_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`generations/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// Still TODO, separately: transform/transcode, CDN, brand check, metadata store…&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The pluggable way:&lt;/strong&gt; hand the same URL to a media pipeline and get a production asset back in one call. (This example uses Cloudinary, but the shape is what matters.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;v2&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cloudinary&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cloudinary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// configured from CLOUDINARY_URL&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;asset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cloudinary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;agent-generations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;modelName&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;            &lt;span class="c1"&gt;// searchable metadata&lt;/span&gt;
  &lt;span class="na"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;crop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fill&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;fetch_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="c1"&gt;// optimised derivative&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secure_url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// permanent original, CDN-delivered&lt;/span&gt;
&lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;secure_url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// optimised WebP thumbnail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fetches the bytes (no expiry race in your code), stores a permanent original, derives renditions on demand, and tags it with its metadata. Pass &lt;code&gt;resource_type: "video"&lt;/code&gt; and the eager transforms become an adaptive stream and a poster frame instead of image crops. Same call for an image from one provider or a clip from another, and the agent ends up holding one stable, branded, findable URL, no per-model recipe required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or generate straight into your asset platform
&lt;/h2&gt;

&lt;p&gt;If the generator and the asset platform are the same system, there's no ephemeral URL to rescue in the first place. That's the idea behind Cloudinary's new &lt;a href="https://cloudinary.com/documentation/image_generation_addon" rel="noopener noreferrer"&gt;&lt;strong&gt;Image Generation API&lt;/strong&gt;&lt;/a&gt;: a single &lt;code&gt;text_to_image&lt;/code&gt; endpoint across best-in-class model families (FLUX.2, Nano Banana, GPT Image, Recraft, Ideogram), much like the routing layer from earlier, except the output lands as a managed asset rather than a disappearing link.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.cloudinary.com/v2/generate/&amp;lt;CLOUD_NAME&amp;gt;/text_to_image &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;API_KEY&amp;gt;:&amp;lt;API_SECRET&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "prompt": "a golden retriever wearing a tiny astronaut helmet",
    "model":  { "family": "flux", "tier": "premium" },
    "target": { "target_type": "managed_asset" }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is a permanent &lt;code&gt;secure_url&lt;/code&gt; plus the &lt;code&gt;asset_id&lt;/code&gt; and &lt;code&gt;public_id&lt;/code&gt; you use everywhere else in Cloudinary. The image is born persisted, optimisable, and ready to deliver; generation and production collapse into one step. (Omit &lt;code&gt;model&lt;/code&gt; for the Nano Banana default; &lt;code&gt;target&lt;/code&gt; defaults to a short-lived temporary asset if you'd rather not persist.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The honest version: if all you need is the cheapest bucket, R2 wins on raw storage cost, and Supabase now bundles basic transforms and a CDN. Reach for a full media platform when the &lt;em&gt;whole&lt;/em&gt; job matters (persist, transform, deliver, keep on-brand, organise) and you'd rather make one call than orchestrate five.&lt;/p&gt;

&lt;p&gt;But the broader lesson outlives any product choice. &lt;strong&gt;In an agentic system, a generated file is an intermediate value, not a deliverable.&lt;/strong&gt; The model is a component, not the finish line. Treat the path from raw output to production asset as a real pipeline (whether you build it from buckets, workers, transcoders and a metadata table, or hand it to something that does all five) and your agents stop storing links to nothing.&lt;/p&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Cloudinary ❤️ developers&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👉 &lt;strong&gt;&lt;a href="https://link.cloudinary.com/uq5u6" rel="noopener noreferrer"&gt;Create your free account&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Stopped Choosing Image Models Manually</title>
      <dc:creator>Ebony Louis</dc:creator>
      <pubDate>Tue, 30 Jun 2026 16:40:40 +0000</pubDate>
      <link>https://dev.to/cloudinary/i-stopped-choosing-image-models-manually-48p7</link>
      <guid>https://dev.to/cloudinary/i-stopped-choosing-image-models-manually-48p7</guid>
      <description>&lt;p&gt;Every few weeks there's a new image model everyone says you should be using: FLUX, Recraft, Ideogram, GPT Image. Each one has different strengths, different tradeoffs, and another decision for developers to make before they've even written a line of code.&lt;/p&gt;

&lt;p&gt;So when I started experimenting with &lt;a href="https://cloudinary.com/documentation/image_generation_addon?utm_source=dev-dot-to&amp;amp;utm_content=imagegen-models-via-console#generating_images_via_the_console" rel="noopener noreferrer"&gt;Cloudinary's new Image Generation API&lt;/a&gt;, I caught myself doing something familiar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;generate_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flux&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I realized I shouldn't be choosing the model at all.&lt;/p&gt;

&lt;p&gt;I've spent the last few years building AI agents that decide what actions to take based on context. They choose tools, call APIs, verify their own work. So I stopped making this decision myself.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with hardcoding models
&lt;/h2&gt;

&lt;p&gt;Most examples, including the ones you'll find in documentation, explicitly choose a model. That makes perfect sense for a quickstart, but production applications don't receive the same prompt over and over again.&lt;/p&gt;

&lt;p&gt;One request might be "create a photorealistic product shot." Another might be "design a conference poster with large readable typography." Another might be "generate a hand drawn sketchnote from these lecture notes." You probably wouldn't use the same model for all three. Different models have genuinely different strengths. Some are better at photorealism, others produce stronger illustrations, others handle typography more effectively.&lt;/p&gt;

&lt;p&gt;So instead of asking which model I should hardcode, I started asking whether my agent should decide instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  My first attempt was simple
&lt;/h2&gt;

&lt;p&gt;I wrote a &lt;code&gt;choose_model()&lt;/code&gt; function that matches keywords in the prompt to model strengths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;choose_model_simple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;prompt_lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prompt_lower&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;poster&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;logo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;typography&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sign&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;family&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ideogram&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;standard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prompt likely needs strong text handling.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prompt_lower&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;photo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;realistic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;portrait&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cinematic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;family&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flux&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;premium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prompt is photorealistic, so Flux is a strong fit.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prompt_lower&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;illustration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sketchnote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;diagram&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;family&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recraft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;standard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prompt sounds illustration-heavy.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;family&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nano-banana&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;standard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No specific requirement detected; using the default.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works for obvious cases. But it breaks the moment prompts get ambiguous, and prompts are almost always a little ambiguous.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9dfda73gxf84vv5lbdob.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9dfda73gxf84vv5lbdob.png" alt="keyword router" width="800" height="171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The keyword router explains why it selected Ideogram before calling Cloudinary.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why keyword matching isn't enough
&lt;/h2&gt;

&lt;p&gt;Try this prompt: "A sketchnote of my lecture on photorealistic rendering techniques."&lt;/p&gt;

&lt;p&gt;A keyword router sees "photorealistic" and picks Flux. But the output you actually want is an illustration. The word photorealistic describes the &lt;em&gt;subject&lt;/em&gt;, not the &lt;em&gt;style&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A keyword router matches words. Your agent understands intent. It understands that the output should be a sketchnote, not a photorealistic rendering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Swapping in Claude
&lt;/h2&gt;

&lt;p&gt;Replacing the keyword router with Claude is one function change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;choose_model_claude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a model router for an image generation pipeline.
Given a prompt, return ONLY a JSON object:
{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;family&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;One sentence.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}
Available families: flux, ideogram, recraft, gpt-image, nano-banana.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Prompt: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The keyword router makes a decision by matching keywords. Claude reasons about the intent of the request before choosing a model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzndkm5vu0uz4tzpz38ff.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzndkm5vu0uz4tzpz38ff.png" alt="diagram" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice what changed. The agent isn't generating the image, it isn't replacing Cloudinary. It's making a routing decision. Cloudinary remains the execution layer while the agent decides which model best fits the request. Because Cloudinary exposes multiple model families through the same API, the router only has to decide &lt;strong&gt;what&lt;/strong&gt; to use, not &lt;strong&gt;how&lt;/strong&gt; to call it.&lt;/p&gt;

&lt;p&gt;This example uses a small routing script to make that decision explicit, but the same idea applies to larger agent workflows. If you're already building agents, model selection can become just another reasoning step before the API call. I wrote it as a standalone script because it's easier to understand, easier to experiment with, and easier to adapt to your own projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I actually learned
&lt;/h2&gt;

&lt;p&gt;The model choice matters more than I expected. The same prompt can produce very different results depending on which model you pick, and those differences aren't subtle. Text rendering in particular is still surprisingly hard. Generating a beautiful image is relatively easy, but generating a beautiful image with readable text is a different problem entirely. &lt;/p&gt;

&lt;p&gt;What I found most interesting was that the routing decision itself was the interesting part, not the generation. Once you separate "which model should I use" from "call the API," that first question becomes something an agent can reason about rather than something you have to answer in advance every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Cloudinary?
&lt;/h2&gt;

&lt;p&gt;I wasn't looking for another image model. I was looking for a consistent interface to multiple image models. Without that, swapping models means learning new APIs, new authentication, and new response shapes.&lt;/p&gt;

&lt;p&gt;With Cloudinary's Image Generation API, the router just returns a family name. Everything else stays the same.&lt;/p&gt;

&lt;p&gt;Generated images also land directly in Cloudinary as managed assets, immediately available for transformation and delivery without me needing to do any extra steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;If you'd like to experiment with this pattern, I've shared the complete routing script as a GitHub Gist.&lt;/p&gt;

&lt;p&gt;The script uses Cloudinary's Image Generation API to select a model, generate the image, and save it as a managed asset.&lt;/p&gt;




&lt;p&gt;To get started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;View the &lt;a href="https://gist.github.com/EbonyLouis/cafc39bf5b438b38585e639df16d6477" rel="noopener noreferrer"&gt;routing script (GitHub Gist)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Read the &lt;a href="https://cloudinary.com/documentation/image_generation_addon?utm_source=dev-dot-to&amp;amp;utm_content=imagegen-models-via-console#generating_images_via_the_console" rel="noopener noreferrer"&gt;Image Generation docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Browse the &lt;a href="https://link.cloudinary.com/uq5vj" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where I think this is going
&lt;/h2&gt;

&lt;p&gt;Every few weeks there's another model everyone says you should be using.&lt;/p&gt;

&lt;p&gt;I don't think any of us will be choosing most of them manually.&lt;/p&gt;

&lt;p&gt;We'll be spending more time teaching our agents how to choose instead.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Cloudinary ❤️ developers&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👉 &lt;strong&gt;&lt;a href="https://link.cloudinary.com/uqvzi" rel="noopener noreferrer"&gt;Create your free account&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
