π Your First Automation
Now that you understand the basic concepts, letβs build your first workflow step by step. This hands-on tutorial will walk you through creating a simple but complete AI automation that transforms casual meeting notes into professional format.
What Weβll Build
Weβll create a workflow that:
- Displays a form for the user to input casual meeting notes
- Waits for user input and captures their response
- Uses a LLM to transform the notes into a professional summary
- Returns the final notes document
This demonstrates a common interactive workflow pattern youβll use in many Loopstack automations.
Estimated time to complete this tutorial: 30-45 minutes
Prerequisites
Before starting this tutorial, make sure you have:
- Created a new App: Following the quick start guideΒ
- OpenAI API Key: This tutorial uses OpenAIβs
gpt-4omodel. Youβll need to add your OpenAI API key to your projectβs.envfile
Resources
You can find the full implementation of this tutorial in the community registry here
https://loopstack.ai/registry/loopstack-meeting-notes-example-workflowΒ
Use this command, if you want to install the complete sources in your project.
loopstack add @loopstack/meeting-notes-example-workflowImplementing it step by step
Step 1: Create the MeetingNotesDocument
First, weβll create the MeetingNotesDocument. This document serves as the input form where users enter their raw, unstructured meeting notes. It provides a simple textarea for capturing the original text before AI processing.
Create the file
src/meeting-notes-example/documents/meeting-notes-document.ts:
import { BlockConfig, WithArguments } from '@loopstack/common';
import { z } from 'zod';
import { DocumentBase } from '@loopstack/core';
import { Injectable } from '@nestjs/common';
export const MeetingNotesDocumentSchema = z.object({
text: z.string(),
});
@Injectable()
@BlockConfig({
configFile: __dirname + '/meeting-notes-document.yaml',
})
@WithArguments(MeetingNotesDocumentSchema)
export class MeetingNotesDocument extends DocumentBase {}In this file:
- We export the
MeetingNotesDocumentSchemaas a Zod schema so it can be reused in the workflowβs@WithStatedecorator - We use the
@Injectable()decorator (from NestJS) to make the document injectable - We use
@BlockConfigto specify the YAML configuration file - We use
@WithArgumentsto define the documentβs properties schema (just atextstring in this case) - The class extends
DocumentBasefrom@loopstack/core
Step 2: Create the MeetingNotesDocument Configuration
Create the file
src/meeting-notes-example/documents/meeting-notes-document.yaml:
type: document
ui:
form:
properties:
text:
title: Text
widget: textarea
actions:
- type: button
widget: button
transition: user_response
options:
label: "Optimize Notes"In this file:
- We specify
type: documentto identify this as a document configuration - We define the UI form under
ui.form.properties, configuring thetextproperty to display as a textarea widget - We add an action button under
ui.actionsthat triggers theuser_responsetransition and displays the label βOptimize Notesβ
Step 3: Create the Meeting Notes Workflow
Now we create the workflow that will use our document. The workflow is the entrypoint to the automation. It acts as a data container for the workflow execution and maintains the state of the workflow.
Create the file
src/meeting-notes-example/meeting-notes.workflow.ts:
import { WorkflowBase } from '@loopstack/core';
import { BlockConfig, Document, Tool, WithArguments, WithState } from '@loopstack/common';
import { MeetingNotesDocument, MeetingNotesDocumentSchema } from './documents/meeting-notes-document';
import { z } from 'zod';
import { CreateDocument } from '@loopstack/core-ui-module';
@BlockConfig({
configFile: __dirname + '/meeting-notes.workflow.yaml',
})
@WithArguments(z.object({
inputText: z.string(),
}))
@WithState(z.object({
meetingNotes: MeetingNotesDocumentSchema.optional(),
}))
export class MeetingNotesWorkflow extends WorkflowBase {
@Tool() createDocument: CreateDocument;
@Document() meetingNotesDocument: MeetingNotesDocument;
}In this file:
- We create the class
MeetingNotesWorkflowwhich extends theWorkflowBaseclass - We use
@BlockConfigto define theconfigFilewhere the workflow transitions are defined - We use
@WithArgumentsto define the input arguments the workflow accepts (in this case,inputText) - We use
@WithStateto define the workflow state schema, which includes themeetingNotesproperty - We use the
@Tool()decorator to inject theCreateDocumenttool for creating/updating documents - We use the
@Document()decorator to inject theMeetingNotesDocumentwe created earlier
Step 4: Create the Workflow Configuration
Next, we define the exact behaviour of the workflow in YAML. Workflows are executed as state machines which gives you precise control over the business logic of the process.
Create the file
src/meeting-notes-example/meeting-notes.workflow.yaml:
title: "Human-in-the-loop Demo (Meeting Notes Optimizer)"
description: "A demo workflow to demonstrate how to use AI to structure meeting notes."
ui:
form:
properties:
inputText:
title: 'Text'
widget: 'textarea'
transitions:
- id: create_form
from: start
to: waiting_for_response
call:
- id: form
tool: createDocument
args:
id: input
document: meetingNotesDocument
update:
content:
text: |
Unstructured Notes:
{{ args.inputText }}
- id: user_response
from: waiting_for_response
to: response_received
trigger: manual
call:
- id: create_response
tool: createDocument
args:
id: input
document: meetingNotesDocument
update:
content: ${ transition.payload }
assign:
meetingNotes: ${ result.data.content }In this file:
- We add a
titleanddescriptionfor the workflow - We define a
ui.formsection that configures how workflow arguments are displayed (theinputTextfield as a textarea) - We define the first transition
create_formfromstarttowaiting_for_response. In this transition we create themeetingNotesDocumentand populate it with the input text using template syntax{{ args.inputText }} - We define the second transition
user_responsefromwaiting_for_responsetoresponse_received. It usestrigger: manualwhich makes the state machine pause execution until the user sends a response - We update the document with the userβs response from
${ transition.payload }and assign it to themeetingNotesstate property
Step 5: Create the Module
Create the module file to register the workflow and document components.
src/meeting-notes-example/meeting-notes-example.module.ts:
import { Module } from '@nestjs/common';
import { LoopCoreModule } from '@loopstack/core';
import { CoreUiModule } from '@loopstack/core-ui-module';
import { MeetingNotesWorkflow } from './meeting-notes.workflow';
import { MeetingNotesDocument } from './documents/meeting-notes-document';
@Module({
imports: [LoopCoreModule, CoreUiModule],
providers: [
MeetingNotesWorkflow,
MeetingNotesDocument,
],
exports: [
MeetingNotesWorkflow,
]
})
export class MeetingNotesExampleModule {}In this file:
- We import the required Loopstack modules:
LoopCoreModuleandCoreUiModule(for the CreateDocument tool) - We register the workflow and the
MeetingNotesDocumentasproviders - We export the workflow so it can be used by other modules
Step 6: Add the Workflow to the Default Workspace
Now we need to add our workflow to the DefaultWorkspace so it becomes accessible in the Loopstack Studio.
Update the file
src/default.workspace.ts:
import { WorkspaceBase } from '@loopstack/core';
import { Injectable } from '@nestjs/common';
import { BlockConfig, Workflow } from '@loopstack/common';
import { HelloWorldWorkflow } from './hello-world/hello-world.workflow';
import { MeetingNotesWorkflow } from './meeting-notes-example/meeting-notes.workflow';
@Injectable()
@BlockConfig({
config: {
title: "Default Workspace"
}
})
export class DefaultWorkspace extends WorkspaceBase {
@Workflow() helloWorld: HelloWorldWorkflow;
// Add your workflows here
@Workflow() meetingNotesWorkflow: MeetingNotesWorkflow;
}What we changed:
- We imported our
MeetingNotesWorkflowclass - We added a new
@Workflow()decorated property to register our workflow in the workspace
Step 7: Import the Module into the Default Module
Next, we need to import our MeetingNotesExampleModule into the DefaultModule so all components are available.
Update the file
src/default.module.ts:
import { Module } from '@nestjs/common';
import { LoopCoreModule } from '@loopstack/core';
import { CoreUiModule } from '@loopstack/core-ui-module';
import { DefaultWorkspace } from './default.workspace';
import { HelloWorldWorkflow } from './hello-world/hello-world.workflow';
import { MeetingNotesExampleModule } from './meeting-notes-example/meeting-notes-example.module';
@Module({
imports: [LoopCoreModule, CoreUiModule, MeetingNotesExampleModule],
providers: [
DefaultWorkspace,
HelloWorldWorkflow,
],
})
export class DefaultModule {}What we changed:
- We imported the
MeetingNotesExampleModule - We added it to the
importsarray so that all the workflow and document providers become available
Step 8: Run the workflow
We can now run the first version of our workflow.
- Open the Loopstack Studio at http://localhost:3000Β (or where you are running it)
- Go to
Workspaces - Open or create a new βDefault Workspaceβ
- Click on
Runat the top right and select our newly createdMeetingNotesWorkflowcalled βHuman-in-the-loop Demo (Meeting Notes Optimizer)β
As you can see this workflow does not do much yet. So far, it only allows you to change the meeting notes that were created as a document. Letβs add some AI magic to itβ¦
Step 9: Create the OptimizedNotesDocument
Now we need to create the OptimizedNotesDocument. This document represents the structured output that the AI will generate from the raw meeting notes. It contains separate fields for the meeting date, summary, participants, decisions made, and action items - providing a professional, organized format for the meeting information.
Create the file
src/meeting-notes-example/documents/optimized-notes-document.ts:
import { BlockConfig, WithArguments } from '@loopstack/common';
import { z } from 'zod';
import { DocumentBase } from '@loopstack/core';
import { Injectable } from '@nestjs/common';
export const OptimizedMeetingNotesDocumentSchema = z.object({
date: z.string(),
summary: z.string(),
participants: z.array(z.string()),
decisions: z.array(z.string()),
actionItems: z.array(z.string()),
});
@Injectable()
@BlockConfig({
configFile: __dirname + '/optimized-notes-document.yaml',
})
@WithArguments(OptimizedMeetingNotesDocumentSchema)
export class OptimizedNotesDocument extends DocumentBase {}In this file:
- We define a schema with multiple properties:
date,summary,participants,decisions, andactionItems - The schema uses arrays for participants, decisions, and action items to capture multiple entries
- We export the schema so it can be reused in the workflowβs state
Step 10: Create the OptimizedNotesDocument Configuration
Create the file
src/meeting-notes-example/documents/optimized-notes-document.yaml:
type: document
ui:
form:
order:
- date
- summary
- participants
- decisions
- actionItems
properties:
date:
title: Date
summary:
title: Summary
widget: textarea
participants:
title: Participants
collapsed: true
items:
title: Participant
decisions:
title: Decisions
collapsed: true
items:
title: Decision
actionItems:
title: Action Items
collapsed: true
items:
title: Action Item
actions:
- type: button
widget: button
transition: confirm
options:
label: "Confirm"In this file:
- We specify the
orderof fields in the form - We configure each propertyβs display settings (title, widget type, collapsed state for arrays)
- Array properties use the
itemsconfiguration to define how each item is displayed - We add a button action that triggers the
confirmtransition
Step 11: Update the Workflow to include the OptimizedNotesDocument
Now we need to update our workflow to include the new document and the AI tool.
Update the file
src/meeting-notes-example/meeting-notes.workflow.ts:
import { WorkflowBase } from '@loopstack/core';
import { BlockConfig, Document, Tool, WithArguments, WithState } from '@loopstack/common';
import { MeetingNotesDocument, MeetingNotesDocumentSchema } from './documents/meeting-notes-document';
import { OptimizedNotesDocument, OptimizedMeetingNotesDocumentSchema } from './documents/optimized-notes-document';
import { z } from 'zod';
import { AiGenerateDocument } from '@loopstack/ai-module';
import { CreateDocument } from '@loopstack/core-ui-module';
@BlockConfig({
configFile: __dirname + '/meeting-notes.workflow.yaml',
})
@WithArguments(z.object({
inputText: z.string(),
}))
@WithState(z.object({
meetingNotes: MeetingNotesDocumentSchema.optional(),
optimizedNotes: OptimizedMeetingNotesDocumentSchema.optional(),
}))
export class MeetingNotesWorkflow extends WorkflowBase {
@Tool() aiGenerateDocument: AiGenerateDocument;
@Tool() createDocument: CreateDocument;
@Document() meetingNotesDocument: MeetingNotesDocument;
@Document() optimizedNotesDocument: OptimizedNotesDocument;
}What we changed:
- We imported the
OptimizedNotesDocumentand its schema - We imported the
AiGenerateDocumenttool from@loopstack/ai-module - We added
optimizedNotesto the workflow state - We added the
@Tool()decorator foraiGenerateDocument - We added the
@Document()decorator foroptimizedNotesDocument
Step 12: Update the Module to include the OptimizedNotesDocument
Update the file
src/meeting-notes-example/meeting-notes-example.module.ts:
import { Module } from '@nestjs/common';
import { LoopCoreModule } from '@loopstack/core';
import { CoreUiModule } from '@loopstack/core-ui-module';
import { AiModule } from '@loopstack/ai-module';
import { MeetingNotesWorkflow } from './meeting-notes.workflow';
import { MeetingNotesDocument } from './documents/meeting-notes-document';
import { OptimizedNotesDocument } from './documents/optimized-notes-document';
@Module({
imports: [LoopCoreModule, CoreUiModule, AiModule],
providers: [
MeetingNotesWorkflow,
MeetingNotesDocument,
OptimizedNotesDocument,
],
exports: [
MeetingNotesWorkflow,
]
})
export class MeetingNotesExampleModule {}What we changed:
- We imported the
AiModulefor theAiGenerateDocumenttool - We added
AiModuleto theimportsarray - We added
OptimizedNotesDocumentto theprovidersarray
Step 13: Finalize the Workflow configuration
Now letβs add the AI processing transitions to complete the workflow.
Update the file
src/meeting-notes-example/meeting-notes.workflow.yaml:
title: "Human-in-the-loop Demo (Meeting Notes Optimizer)"
description: "A demo workflow to demonstrate how to use AI to structure meeting notes."
ui:
form:
properties:
inputText:
title: 'Text'
widget: 'textarea'
transitions:
- id: create_form
from: start
to: waiting_for_response
call:
- id: form
tool: createDocument
args:
id: input
document: meetingNotesDocument
update:
content:
text: |
Unstructured Notes:
{{ args.inputText }}
- id: user_response
from: waiting_for_response
to: response_received
trigger: manual
call:
- id: create_response
tool: createDocument
args:
id: input
document: meetingNotesDocument
update:
content: ${ transition.payload }
assign:
meetingNotes: ${ result.data.content }
- id: optimize_notes
from: response_received
to: notes_optimized
call:
- id: prompt
tool: aiGenerateDocument
args:
llm:
provider: openai
model: gpt-4o
response:
id: final
document: optimizedNotesDocument
prompt: |
Extract all information from the provided meeting notes into the structured document.
<Meeting Notes>
{{ meetingNotes.text }}
</Meeting Notes>
assign:
optimizedNotes: ${ result.data.content }
- id: confirm
from: notes_optimized
to: end
trigger: manual
call:
- id: create_response
tool: createDocument
args:
id: final
document: optimizedNotesDocument
update:
content: ${ transition.payload }
assign:
optimizedNotes: ${ result.data.content }What we added:
- The
optimize_notestransition which executes when the user clicks the button and the workflow transitions toresponse_received - In this transition we call the
aiGenerateDocumenttool from the AI module - We configure the LLM to use
gpt-4ofromopenai - We specify the
responseconfiguration with anidand the targetdocumenttype (optimizedNotesDocument) - We provide a prompt that includes the original
meetingNotes.textusing template syntax - The AI-generated document is assigned to the
optimizedNotesstate property - The
confirmtransition withtrigger: manualwaits for the user to review and confirm the generated document
Step 14: Run the workflow (again)
Letβs run our final workflow version.
- Return to the Loopstack Studio
- Open the βDefault Workspaceβ
- Click on
Runand select our updated workflow
You will see:
- The input form with the textarea
- When you click
Optimize Notes, the AI will generate the structuredOptimizedNotesDocument - The final notes document is displayed with fields for date, summary, participants, decisions, and action items
- You can edit any field and click
Confirmto finalize the document