How to Build a Mica Workflow
How to leverage Mica (Monterosa Interaction Cloud Assistant) to build your own AI powered automations
This guide walks you through building a Mica AI Assistant Workflow using AWS Lambda. We'll create the "Spot the Ball" demo workflow as an example, though the same principles apply to any interactive workflow.
About the Spot the Ball Demo

"Spot the Ball" is an AI-powered workflow that automates the creation of interactive trivia content. This process leverages AI to:
Remove the ball from a sports photograph
Generate multiple choice answer options
Create an engaging trivia question format
This automation saves valuable content creation time by eliminating the need for manual photo editing and question generation. What traditionally might take 30 minutes per image can now be accomplished in seconds.
Example showing original image with ball → AI processing → image without ball + multiple choice overlay
Prerequisites
Familiarity with AWS Lambda basics
Understanding of JavaScript/Node.js
Access to AWS Console
Overview
A Mica Workflow is an HTTP endpoint that creates interactive dialogues within the Monterosa / Studio UI. The workflow receives POST requests with user context and responds with JSON that defines the UI blocks and available actions.
Key concepts:
Stateless communication: Each request is independent
Payload persistence: Data carries forward between requests
Block-based UI: Response defines visual components
Action-driven flow: User interactions trigger new requests
Step 1: Set Up the Lambda Function
Create a new AWS Lambda function with the following configuration:
Runtime: Node.js (latest version)
Create a Lambda function URL to make it publicly accessible
Set CORS headers to allow Studio to communicate with your endpoint
Step 2: Implement Authentication
Every workflow must authenticate requests to prevent unauthorised access. The authentication flow:
Extract the Authorisation header from the request
Use the token to call the Studio API's
/api/v2/meendpointVerify the user has appropriate permissions for the requested resource
// NOTE:
// This is a shortened version of the function, refer to the full listing below
// to get the full one with logging and error handling.
async function getAuthenticatedUser(event) {
const authHeader = event.headers?.Authorization || event.headers?.authorization;
const requestBody = JSON.parse(event.body);
const baseURL = requestBody.context?.baseURL;
const res = await fetch(`${baseURL}/api/v2/me`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: authHeader,
},
});
if (!res.ok) return null;
const data = await res.json();
return data?.data;
}For role-based authorisation, use the /api/v2/{resource}/{id}/my_role endpoints as described in the Authentication and authorisation section.
Step 3: Handle the Initial Request
When a user opens your workflow, Studio sends an initial POST request with no action. This is where you return the first screen:
// No action = initial screen
if (!action) {
return {
blocks: [
{
type: "input",
input_type: "image",
label: "Upload image",
name: "upload_image",
},
// ... more input blocks
],
actions: [
{
type: "button",
label: "Generate",
action_id: "generate_images",
},
],
};
}Step 4: Define Block Types
Blocks are the building components of your workflow UI. Common types include:
Text blocks: Display information with basic HTML formatting
Input blocks: Collect user data (text, images, colours, files)
Image blocks: Show images with optional labels
Loader blocks: Indicate processing status
Alert blocks: Show status messages
See the Blocks section for complete specifications.
Step 5: Handle User Actions
When users interact with your workflow (button clicks), Studio sends a new request with the action field populated:
switch (action) {
case "generate_images":
// Process the user inputs
const optionsCount = parseInt(inputs.options_count) || 3;
return {
blocks: [
// Show generated results
],
actions: [
{ type: "button", label: "Regenerate", action_id: "regenerate_images" },
{ type: "button", label: "Create", action_id: "create_experience" },
],
payload: {
// Store data for next request
hero_image_url: heroImageUrl,
options_count: optionsCount,
},
};
}Step 6: Use Payload for State Management
Since workflows are stateless, use the payload object to maintain data between requests. Any data in the response payload is included in the next request:
// Store data in response
payload: {
attempt: attemptNumber,
hero_image_url: heroImageUrl,
user_inputs: inputs,
}
// Access in next request
const previousData = payload?.hero_image_url;Step 7: Handle Long-Running Operations
For time-intensive operations like image processing, use the task-based polling mechanism:
// Start a long-running task
return {
blocks: [
{ type: "loader", text: "Processing..." }
],
task: {
id: "task_123",
state: "running",
progress: 45,
pollIntervalMs: 2000
}
};See the Tasks section for complete implementation details.
Step 8: Structure Your Response
Every workflow response must follow this structure:
{
blocks: Block[], // UI components to render
actions?: Action[], // Available user actions
autoAction?: AutoAction, // Automatic action to trigger
payload?: object, // Data to persist
task?: Task // Long-running operation status
}Step 9: Add Error Handling
Always handle authentication failures and invalid requests gracefully:
const authUser = await getAuthenticatedUser(event);
if (!authUser) {
return {
statusCode: 200,
body: JSON.stringify({
blocks: [
{
type: "text",
text: "You are not authorised to use this workflow. Please log in.",
},
],
}),
};
}Step 10: Submit for Review
Once your workflow is complete:
Ensure your Lambda function URL is publicly accessible
Submit your workflow name and endpoint URL to the Monterosa team
The workflow will be reviewed using the Verification and approval checklist
Key Implementation Tips
Always authenticate first: Check user authorisation before processing any requests
Use descriptive action IDs: Make your code self-documenting
Handle edge cases: Account for missing inputs or invalid states
Keep responses lightweight: Only include necessary data in blocks
Maintain payload consistency: Ensure data flows correctly between requests
Beyond the Example
While this guide uses the "Spot the Ball" workflow as an example, you can build workflows for any interactive process:
Content summarisation
Data analysis and visualisation
Multi-step form wizards
Approval workflows
Integration with external APIs
The key is understanding the request/response cycle and building appropriate UI blocks for your use case.
Complete Code Example
// Set up CORS headers
const corsHeaders = {
"Access-Control-Allow-Origin": "*", // Allow requests from any origin
"Access-Control-Allow-Headers":
"Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
"Access-Control-Allow-Methods": "OPTIONS,POST,GET",
};
export const handler = async (event) => {
console.log("Received event:", JSON.stringify(event, null, 2));
// Handle preflight OPTIONS request
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers: corsHeaders,
body: JSON.stringify({}),
};
}
const authUser = await getAuthenticatedUser(event);
if (!authUser) {
return generateResponse(200, {
blocks: [
{
type: "text",
text: "You are not authorised to use this workflow. Please log in.",
},
],
});
}
let action = "";
let inputs = {};
let payload = {};
if (event.body) {
const requestBody = JSON.parse(event.body);
action = requestBody.action || "";
inputs = requestBody.inputs || {};
payload = requestBody.payload || {};
}
// If no action is provided, return the initial workflow UI
if (!action) {
return generateResponse(200, {
blocks: [
{
type: "input",
input_type: "image",
label: "Upload image",
name: "upload_image",
},
{
type: "input",
input_type: "text",
label: "Number of answer options",
name: "options_count",
},
{
type: "input",
input_type: "colour",
label: "Label colour",
name: "label_colour",
},
],
actions: [
{
type: "button",
label: "Generate",
action_id: "generate_images",
},
],
});
}
switch (action) {
case "generate_images":
case "regenerate_images": {
const previousHeroImageUrl = payload?.hero_image_url;
const { heroImageUrl, revealImageUrl } =
getRandomImagePair(previousHeroImageUrl);
const attemptNumber = payload?.attempt ? payload.attempt + 1 : 1;
const optionsCount = parseInt(inputs.options_count) || 3;
return generateResponse(200, {
blocks: [
{
type: "text",
label: "Question",
text: "Can you spot the ball?",
format: "plain",
},
{
type: "image",
label: "Hero image",
url: heroImageUrl,
alt_text: "Hero Image",
},
{
type: "image",
label: "Reveal image",
url: revealImageUrl,
alt_text: "Reveal Image",
},
{
type: "text",
label: "Correct answer",
text: `Option ${Math.floor(Math.random() * optionsCount) + 1}`,
format: "plain",
},
],
actions: [
{
type: "button",
label: "Regenerate",
action_id: "regenerate_images",
},
{ type: "button", label: "Create", action_id: "create_experience" },
],
payload: {
attempt: attemptNumber,
hero_image_url: heroImageUrl,
reveal_image_url: revealImageUrl,
// Store the original inputs to maintain them across requests
upload_image: inputs.upload_image,
options_count: inputs.options_count,
label_colour: inputs.label_colour,
},
});
}
case "create_experience": {
// This action only returns a loading state screen.
// In a real implementation, you would use the payload data to create the experience.
console.log(
"Creating experience with payload:",
JSON.stringify(payload, null, 2),
);
return generateResponse(200, {
blocks: [
{
type: "text",
text: "Building your experience...",
format: "plain",
},
{
type: "loader",
text: "Please wait while we prepare your experience.",
},
{
type: "image",
label: "Using hero image",
url: payload.hero_image_url,
alt_text: "Hero Image",
},
{
type: "image",
label: "Using reveal image",
url: payload.reveal_image_url,
alt_text: "Reveal Image",
},
{
type: "text",
label: "Number of options",
text: payload.options_count,
format: "plain",
},
{
type: "text",
label: "Label colour",
text: payload.label_colour,
format: "plain",
},
],
});
}
default:
return generateResponse(400, { error: "Unknown action" });
}
};
// Helper function to generate a response
function generateResponse(statusCode, body, headers = {}) {
return {
statusCode,
headers: {
"Content-Type": "application/json",
...corsHeaders,
},
body: JSON.stringify(body),
};
}
// Helper function to confirm the request is authenticated.
// Returns the user ID and user object if authenticated, otherwise null.
async function getAuthenticatedUser(event) {
try {
const authHeader =
event.headers?.Authorization || event.headers?.authorization;
if (!authHeader) {
console.log("No auth header found");
return null;
}
if (!event.body) {
console.log("Missing request body");
return null;
}
const requestBody = JSON.parse(event.body);
const baseURL = requestBody.context?.baseURL;
if (!baseURL) {
console.log("No base URL found, cannot authenticate user");
return null;
}
const res = await fetch(`${baseURL}/api/v2/me`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: authHeader,
},
});
if (!res.ok) {
console.log("Failed checking user authentication");
return null;
}
const data = await res.json();
const user = data?.data;
const userId = user?.id || user?.attributes?.email || null;
if (!userId) {
console.log("No user ID found in response");
return null;
}
console.log(`User authenticated successfully: ${userId}`);
return { userId, user };
} catch (error) {
console.log("Error checking user authentication", error);
return null;
}
}
// Returns a random image pair from the mock image URLs.
// Could be replaced with a real image generation API call.
// The image provided by the user is ignored in this demo.
function getRandomImagePair(previousHeroImageUrl) {
const mockImageUrls = [
"https://cdn-int24.lvis.io/assets/84/84d940aa-b031-4004-b524-b4fe8848ec96/orig",
"https://cdn-int24.lvis.io/assets/f7/f7c25766-e4d6-4dd7-8cfd-e2b3547241b2/orig",
"https://cdn-int24.lvis.io/assets/f0/f003fdfe-c520-46bc-b11e-30aae5459ea0/orig",
"https://cdn-int24.lvis.io/assets/6f/6f6dee09-50e2-400e-bcbf-8b1dd9023ef7/orig",
];
const mockRevealUrls = [
"https://cdn-int24.lvis.io/assets/44/44cf6897-7b1f-43fd-b881-fb0eae0a7fe9/orig",
"https://cdn-int24.lvis.io/assets/2e/2e7431e9-1073-42f5-ba8d-bf104b520b5c/orig",
"https://cdn-int24.lvis.io/assets/70/70003aad-b5f4-4c55-aba3-33f799f55620/orig",
"https://cdn-int24.lvis.io/assets/8b/8b838e96-dc60-4eef-8cf6-d547412a746e/orig",
];
let index = Math.floor(Math.random() * mockImageUrls.length);
// Make sure we don't get the same image as the previous one for better demo.
while (
mockImageUrls[index] === previousHeroImageUrl &&
mockImageUrls.length > 1
) {
index = Math.floor(Math.random() * mockImageUrls.length);
}
return {
heroImageUrl: mockImageUrls[index],
revealImageUrl: mockRevealUrls[index],
};
}If you've followed the steps above, you should now be ready to build and deploy your own Workflow.
Last updated

