Skip to main content

Bulk AI Flow

This plugin allows filling fields in multiple selected records based on data from other fields using LLM. This also supports vision tasks so you can ask it to e.g. detect dominant color on image or describe what is on the image. Plugin supports classification to enum options automatically.

Installation

To install the plugin:

npm install @adminforth/bulk-ai-flow --save

You'll also need an image vision adapter:

npm install @adminforth/image-vision-adapter-openai --save

Vision mode

This mode covers next generations:

- Image(one or many fields) -> to -> Text/Number/Enum/Boolean(one or many fields)
- Image(one or many fields) + Text/Number/Enum/Boolean(one or many fields) -> to -> Text/Number/Enum/Boolean(one or many fields)

Lets try both. Add a column for storing the URL or path to the image in the database, add this statement to the ./schema.prisma:

./schema.prisma
model apartments {
id String @id
created_at DateTime?
title String
square_meter Float?
price Decimal
number_of_rooms Int?
description String?
country String?
listed Boolean
realtor_id String?
apartment_image String?
}

Migrate prisma schema:

npm run makemigration -- --name add-apartment-image-url ; npm run migrate:local

We will also attach upload plugin to this field.

Add credentials in your .env file:

.env
...

OPENAI_API_KEY=your_secret_openai_key

...

Add column to aparts resource configuration:

./resources/apartments.ts
import BulkAiFlowPlugin  from '@adminforth/bulk-ai-flow';
import AdminForthImageVisionAdapterOpenAi from '@adminforth/image-vision-adapter-openai';
import UploadPlugin from '@adminforth/upload';
import { randomUUID } from 'crypto';
import AdminForthAdapterS3Storage from '@adminforth/storage-adapter-amazon-s3'

export const admin = new AdminForth({
...
resourceId: 'aparts',
columns: [
...
{
name: 'apartment_image',
label: 'Image',
showIn: { list: false, create: true, edit: true},
}
...
],
plugins: [
...
new UploadPlugin({
storageAdapter: new AdminForthAdapterS3Storage({
bucket: process.env.AWS_BUCKET_NAME,
region: process.env.AWS_REGION,
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
s3ACL: 'public-read',
}),
pathColumnName: 'apartment_image',
allowedFileExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webm', 'webp'],
filePath: ({originalFilename, originalExtension, contentType}) =>
`aparts/${new Date().getFullYear()}/${randomUUID()}-${originalFilename}.${originalExtension}`,
}),

new BulkAiFlowPlugin({
actionName: 'Analyze',
attachFiles: async ({ record }: { record: any }) => {
if (!record.apartment_image) {
return [];
}
return [`https://tmpbucket-adminforth.s3.eu-central-1.amazonaws.com/${record.apartment_image}`];
},
visionAdapter: new AdminForthImageVisionAdapterOpenAi(
{
openAiApiKey: process.env.OPENAI_API_KEY as string,
model: 'gpt-5-mini',
}
),
fillFieldsFromImages: {
'description': 'describe what is in the image, also take into account that price is {{price}}',
'country': 'In which country it can be located?',
'number_of_rooms': 'How many rooms are in the apartment? Just try to guess what is a typical one. If you do not know, just guess',
'square_meter': 'Try to guess what is the typical square of the apartment in square meters? If you do not know, just guess',
'listed': 'Is the apartment should be listed for sale? If you do not know, just guess, return boolean value',
},
}),
],


...

});

⚠️ Make sure your attachFiles function returns a valid array of image URLs or an empty array. Returning anything else may cause an error.

Usage

  1. Select fields you want to fill
  2. Click on the three dots menu
  3. Click analyze alt text
  4. Wait for finish analyze
  5. Check and edit result alt text
  6. Save changhes alt text

Text-to-Text Processing

This is the most basic plugin usage. You can connect any text completion adapter to fill one or several string/number/boolean fields from other fields.

Example: Translate Names to English

Normalize user names by translating them from any language to English for internal processing.

import CompletionAdapterOpenAIChatGPT from '@adminforth/completion-adapter-open-ai-chat-gpt/index.js';

// Add to your resource plugins array
new BulkAiFlowPlugin({
actionName: 'Translate surnames',
textCompleteAdapter: new CompletionAdapterOpenAIChatGPT({
openAiApiKey: process.env.OPENAI_API_KEY as string,
model: 'gpt-5-nano',
extraRequestBodyParameters: {
temperature: 0.7
}
}),
fillPlainFields: {
'full_name_en': 'Translate this name to English: {{users_full_name}}',
},
}),

Image-to-Text Analysis (Vision)

Analyze images and extract information to fill text, number, enum, or boolean fields.

Example: Age Detection from Photos

import AdminForthImageVisionAdapterOpenAi from '@adminforth/image-vision-adapter-openai/index.js';

// Add to your resource plugins array
new BulkAiFlowPlugin({
actionName: 'Guess age',
visionAdapter: new AdminForthImageVisionAdapterOpenAi({
openAiApiKey: process.env.OPENAI_API_KEY as string,
model: 'gpt-5-mini',
}),
fillFieldsFromImages: {
'age': 'Analyze the image and estimate the age of the person. Return only a number.',
},
attachFiles: async ({ record }) => {
if (!record.image_url) {
return [];
}
return [`https://users-images.s3.eu-north-1.amazonaws.com/${record.image_url}`];
},
}),

Text-to-Image generation or image editing

Generate new images based on existing data and/or images using AI image generation adapters.

Example: Creating Cartoon Avatars

import ImageGenerationAdapterOpenAI from '@adminforth/image-generation-adapter-openai/index.js';

// Add to your resource plugins array
new BulkAiFlowPlugin({
actionName: 'Generate cartoon avatars',
imageGenerationAdapter: new ImageGenerationAdapterOpenAI({
openAiApiKey: process.env.OPENAI_API_KEY as string,
model: 'gpt-image-1.5',
}),
attachFiles: async ({ record }) => {
if (!record.user_photo) {
return [];
}
return [`https://bulk-ai-flow-playground.s3.eu-north-1.amazonaws.com/${record.users_photo}`];
},
generateImages: {
users_avatar: {
prompt: 'Transform this photo into a cartoon-style avatar. Maintain the person\'s features but apply cartoon styling. Do not add text or logos.',
outputSize: '1024x1024',
countToGenerate: 2,
rateLimit: '3/1h'
},
},
bulkGenerationRateLimit: "1/1h"
}),

Rate Limiting and Best Practices

  • Use rateLimit for individual image generation operations and for the bulk image generation
  new BulkAiFlowPlugin({
...

generateImages: {
users_avatar: {
... //image re-generation limits
rateLimit: '1/5m' // one request per 5 minutes
}
},

...

rateLimits: { // bulk generation limits
fillFieldsFromImages: "5/1d", // 5 requests per day
fillPlainFields: "3/1h", // 3 requests per one hour
generateImages: "1/2m", // 2 request per one minute
}

...

})
  • Consider using lower resolution (512x512) for faster generation and lower costs
  • Test prompts thoroughly before applying to large datasets

Comparing new and old images

If you want to compare a generated image with an image stored in your storage, you need to add the preview prop in your upload plugin setup:

  new UploadPlugin({
...
preview: {
previewUrl: ({filePath}) => `https://static.my-domain.com/${filePath}`,
}
...
})

After generation, you’ll see a button labeled "old image". Clicking it will open a pop-up where you can compare the generated image with the stored one:

alt text

Extra data fields

When creating default prompts, you can use Handlebars to pass data from the record into the prompt:

  ...
fillPlainFields: {
description: 'Create a description based on the apartment name: {{title}}.'
},
...

Each record will receive a unique prompt based on its own title.

If you want to add extra data fields for use in your prompt, implement the provideAdditionalContextForRecord callback:

provideAdditionalContextForRecord({ record, adminUser, resource }) {
const extraData: any = {};
if (record.country === 'Spain') {
// Your logic
// e.g. extraData.discount = '10%';
} else {
// Your logic
}
return extraData;
},
fillPlainFields: {
description: 'Create a description based on the apartment name: {{title}}. Also include the fact that the apartment has a discount of {{extraData.discount}}.'
},

Allow users to edit generation prompts

If you want to let users adjust generation prompts for their unique cases, add this to the plugin setup:

...
askConfirmationBeforeGenerating: true,
...

The user will now see a popup with a "Start generation" button and an "Edit prompts" button, allowing them to modify the prompt before running it.

alt text alt text

☝️ Updated prompts are stored in the user's local storage. Changes are local to that browser and do not affect other users or devices.

Processing big sets of data ( filtered records )

There might be cases when you want to process more records than can fit your list view. Here you can use the recordSelector param. It can be checkbox (default) or filtered. filtered uses all filtered records for generation, so you can even process the whole resource.

        new BulkAiFlowPlugin({          
actionName: 'Generate description and Price',

recordSelector: 'filtered', // default is 'checkbox'

...

});

❗️❗️❗️ Using recordSelector: 'filtered' might be expensive. Before processing large data sets, we recommend starting with smaller sets to make sure everything is fine.

Limiting amount of parallel requests using p-limit

If you are processing large sets of data, you might want to limit the number of parallel requests. For this, you can use the concurrencyLimit param:

        new BulkAiFlowPlugin({          
actionName: 'Generate description and Price',

concurrencyLimit: 5, //default is 10

...

});

And there won't be more than 5 parallel requests being handled.

Confirming long-running generations

For very large datasets, you can pause generation at specific checkpoints so users can review results before continuing. Use the askConfirmation option to define confirmation breakpoints by processed record count.

new BulkAiFlowPlugin({
actionName: 'Generate descriptions',
// ...adapters + fields

askConfirmation: [
{ afterRecords: 10 },
{ afterRecords: 30 },
{ everyRecords: 1000 },
],
});

How it works:

  • afterRecords: N — show a confirmation once, after the first $N$ records.
  • everyRecords: N — show a confirmation after every $N$ records.