Build a DnD Character & Campaign Generator Flow
In this step‑by‑step guide we’ll craft a public Flow that:
- Asks the user for the vibe of their next adventure (high‑fantasy, grim‑dark, space‑opera… you name it).
- Creates structured JSON describing N playable characters plus a short campaign arc.
- Illustrates each character with AI‑generated portraits.
- Packages the story bible & artwork into a single zip for easy sharing.
Along the way you’ll see:
- How the Flow language lets you pass data between steps with JSON Path & operators.
- How to configure
ObjectGenerator,ImageGenerator, andZippersteps. - How to wire up the UI so users see portraits, JSON, and a download button when the run finishes.
Create the skeleton Flow
Every Flow starts with top‑level metadata. We’ll mark this Flow as public, charge $3 USD per run, and give it a catchy name.
{
title: 'Forge My Adventuring Party!',
description: 'Generate a full DnD party, their portraits, and a starter campaign hook.',
public: true,
slug: 'dnd-adventure-generator',
runPrice: 3,
runPriceCurrency: 'USD',
payoutMethod: 'Wallet',
// we’ll fill these fields next:
input: {},
steps: [],
ui: {},
}Tip: See the Flow introduction for every root‑level field (runPrice, ui, etc.).
Define user inputs
We want just three inputs:
Field Type Notes
theme: string (required) - “High‑fantasy heist”, “post‑apocalyptic western”…characters: number (min:2 max:6) - How many playable heroes to create.referenceDatasetId: string (dataset) - Optional. Users may upload reference images for portrait style.
"input": {
"type": "object",
"required": ["theme", "characters"],
"properties": {
"theme": {
"type": "string",
"title": "What’s the vibe of your world?",
"placeholder": "Gothic horror in a flooded realm"
},
"characters": {
"type": "integer",
"minimum": 2,
"maximum": 6,
"title": "How many heroes?"
},
"referenceDatasetId": {
"type": "string",
"dataset": true,
"title": "Upload style reference images (optional)"
}
}
}When this Flow is rendered on the public page the dataset: true field (referenceDatasetId) turns into a drag‑and‑drop uploader. See the full Input schema guide for reference.
Add the ObjectGenerator step
The ObjectGenerator will return:
{
party: [
/* N character objects */
],
campaign: {
/* one‑shot hook */
},
}Define a JSON Schema describing that shape and include it in the schema field:
{
id: 'charsGenerator',
type: 'ObjectGenerator',
parameters: {
description: 'You are a veteran Dungeon Master. Create deep, interesting characters and a gripping campaign hook that matches with them.',
input: '$.input.theme',
schema: {
type: 'object',
required: ['party', 'campaign'],
properties: {
party: {
type: 'array',
items: {
type: 'object',
required: ['name', 'race', 'class', 'backstory', 'portraitPrompt'],
properties: {
name: { type: 'string' },
race: { type: 'string' },
class: { type: 'string' },
backstory: { type: 'string' },
portraitPrompts: {
type: 'array',
items: {
type: 'string',
},
},
},
},
},
campaign: {
type: 'object',
required: ['title', 'summary'],
properties: {
title: { type: 'string' },
summary: { type: 'string' },
},
},
},
},
},
}The LLM will be called with the schema, guaranteeing each character has the required fields. Learn more in the ObjectGenerator docs.
Generate portraits for each hero
We’ll map over the party array returned by charsGenerator and then map again over the portraitPrompts array to feed ImageGenerator with all prompts.
{
id: 'portraits',
type: 'ImageGenerator',
parameters: {
baseModel: 'stabilityai/stable-diffusion-2-1',
images: [
[
'@Map($.results.charsGenerator.party)',
[
'@Map($$.portraitPrompts)',
{
prompt: '$$.portraitPrompt',
negativePrompt: 'lowres, bad hands',
steps: 25,
width: 768,
height: 1024,
},
],
],
],
},
}Because steps run sequentially, portraits can safely read from charsGenerator (which has already finished). If you tried to read a future step here the Flow would throw an error.
Dive deeper into mapping,
@Index, and other operators in the Flow language reference.
Bundle everything with Zipper
The final step pulls:
- All portrait images (step
portraits) - The JSON party + campaign (step
charsGenerator)
{
id: 'zipOutput',
type: 'Zipper',
parameters: {
filename: 'dnd_party.zip',
sources: ['portraits', 'charsGenerator'],
},
}The handler grabs each file, bundles it all into a zip file and returns a single presigned URL ready for download.
Configure the results UI
We want to display the generated images, the adventure JSON and the download link for the zip file:
"ui": {
"submitLabel": "Summon Party",
"results": {
"widgets": [
{ "type": "Images", "sources": ["portraits"] },
{ "type": "Objects", "sources": ["charsGenerator"] },
{ "type": "Download","sources": ["zipOutput"] }
]
}
}Add background or preview assets directly through the dashboard; their IDs will appear on the Flow’s JSON automatically. See the UI guide.
The complete Flow
{
title: 'Forge My Adventuring Party!',
description: 'Generate a full DnD party, their portraits, and a starter campaign hook.',
public: true,
slug: 'dnd-adventure-generator',
runPrice: 3,
runPriceCurrency: 'USD',
payoutMethod: 'Wallet',
ui: {
submitLabel: 'Summon Party',
results: {
widgets: [
{ type: 'Images', sources: ['portraits'] },
{ type: 'Objects', sources: ['charsGenerator'] },
{ type: 'Download', sources: ['zipOutput'] },
],
},
},
input: {
type: 'object',
required: ['theme', 'characters'],
properties: {
theme: {
type: 'string',
title: 'What’s the vibe of your world?',
placeholder: 'Gothic horror in a flooded realm',
},
characters: {
type: 'integer',
minimum: 2,
maximum: 6,
title: 'How many heroes?',
},
referenceDatasetId: {
type: 'string',
dataset: true,
title: 'Upload style reference images (optional)',
},
},
},
steps: [
{
id: 'charsGenerator',
type: 'ObjectGenerator',
parameters: {
description: 'You are a veteran Dungeon Master. Create deep, interesting characters and a gripping campaign hook that matches with them.',
input: '$.input.theme',
schema: {
type: 'object',
required: ['party', 'campaign'],
properties: {
party: {
type: 'array',
items: {
type: 'object',
required: [
'name',
'race',
'class',
'backstory',
'portraitPrompt',
],
properties: {
name: { type: 'string' },
race: { type: 'string' },
class: { type: 'string' },
backstory: { type: 'string' },
portraitPrompts: {
type: 'array',
items: {
type: 'string',
},
},
},
},
},
campaign: {
type: 'object',
required: ['title', 'summary'],
properties: {
title: { type: 'string' },
summary: { type: 'string' },
},
},
},
},
},
},
{
id: 'portraits',
type: 'ImageGenerator',
parameters: {
baseModel: 'stabilityai/stable-diffusion-2-1',
images: [
[
'@Map($.results.charsGenerator.party)',
[
'@Map($$.portraitPrompts)',
{
prompt: '$$.portraitPrompt',
negativePrompt: 'lowres, bad hands',
steps: 25,
width: 768,
height: 1024,
},
],
],
],
},
},
{
id: 'zipOutput',
type: 'Zipper',
parameters: {
filename: 'dnd_party.zip',
sources: ['portraits', 'charsGenerator'],
},
},
],
}Save this JSON via Flows → New in the dashboard, attach any background images, and hit Save. Your shiny new Flow is now live at: https://lensless.io/<YOUR-SLUG>
Next ideas
- Add environment concept art: create a second
ImageGeneratorthat maps over the campaign scenes or include dedicated fields for ambient prompts during the object generation. - Fine‑tune on custom art style: if the user supplies references, train the model before generating portraits.
- Paid tiers: change
runPriceto experiment with monetization.