π 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
- Return 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:
- Development server running: Your local Loopstack development server should be running
- Loopstack Studio access: You should be able to access the Loopstack Studio at https://app.loopstack.aiΒ and have connected your local environment
- 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 default template that you install via
npx create-loopstack-app <project_name> or in the template repository on github: https://github.com/loopstack-ai/app-templateΒ .
src/example-module/tutorials/meeting-notes-example/
βββ meeting-notes.workflow.ts
βββ meeting-notes.workflow.yaml
βββ documents/
βββ meeting-notes-document.ts
βββ meeting-notes-document.yaml
βββ optimized-notes-document.ts
βββ optimized-notes-document.yamlTo complete this tutorial:
- Delete or rename the
meeting-notes-exampledirectory, so you can build everything from scratch- Or simply reproduce the steps in the existing tutorial code.
Letβs begin
Step 1: Create the Meeting Notes Workflow
Create the file
src/example-module/tutorials/meeting-notes-example/meeting-notes.workflow.ts:
import { Expose } from 'class-transformer';
import { CreateDocument, Workflow } from '@loopstack/core';
import { BlockConfig, Input } from '@loopstack/shared';
import { AiGenerateDocument } from '@loopstack/llm';
import { MeetingNotesDocument } from './documents/meeting-notes-document';
@BlockConfig({
imports: [
AiGenerateDocument,
CreateDocument,
MeetingNotesDocument,
],
config: {
title: 'Tutorial 1: Meeting Notes',
},
configFile: __dirname + '/meeting-notes.workflow.yaml',
})
export class MeetingNotesWorkflow extends Workflow {
@Input()
@Expose()
meetingNotes: MeetingNotesDocument;
}This workflow is the entrypoint to the automation. It acts as a data container for the workflow execution and maintains the state of the workflow which includes the initial document
meetingNotes. Later, we will add the final, optimized notes document optimizedNotes here too.
In this file:
- we create the service
MeetingNotesWorkflowwhich extends theWorkflowclass. - we import several
ToolsandDocumentsin theBlockConfigthat are needed in the workflow. - we give the workflow the title βTutorial 1: Meeting Notesβ.
- we define the
configFilewhere we will define the exact workflow that will be executed. - we define the property
meetingNoteswhich contain the original notes the user defines. - we decorated the
meetingNotesproperty with@Input()and@Expose(). @Input() allows setting this property from within the workflow run using theassignmethod. @Expose() in turn allows us to access the value from within the workflow via template variables. - later, we will add a second property which will contain the optimized notes, generated with the help of a LLM.
Step 2: Create the Workflow Configuration
Next, we define the exact behaviour of the workflow in YAML.
Create the file
src/example-module/tutorials/meeting-notes-example/meeting-notes.workflow.yaml:
transitions:
- id: create_form
from: start
to: waiting_for_response
call:
- id: form
tool: CreateDocument
args:
document: MeetingNotesDocument
update:
content:
text: |
meeting 1.1.2025
budget: need 2 cut costs sarah said
hire new person?? --> marketing
vendor pricing - follow up needed by anna
mike's project delayed again. deadline moved - when??? next month maybe
client wants faster delivery but budget tight. can we do it?
- id: user_response
from: waiting_for_response
to: response_received
when: manual
call:
- id: create_response
tool: CreateDocument
args:
document: MeetingNotesDocument
update:
content: ${ ctx.payload.transition.payload }
assign:
meetingNotes: ${ result.data.content }In the workflow configuration we define the transitions that the workflow will go through. Workflows are executed as state machines which gives you precise control maintaining the business logic of the process.
The first part of this config includes two transitions create_form and user_response. These two transitions allow us to display an initial document as a form and wait for the user to confirm or edit the content of this document via the frontend.
In this case we create and display the content of the Document MeetingNotesDocument.
In this file:
- we define the first transition from
start(of the workflow) towaiting_for_response. In this transition we create theMeetingNotesDocumentand populate it with a default value. - next, we define the second transition from
waiting_for_responseto the placeresponse_received. It is amanualtransition defined with the propertywhenwhich makes the state machine pause the execution here until the user sends a response. - we update the
MeetingNotesDocumentby creating an updated document and add the content from the transition payload (which is the user response) - finally, the updated document is stored in the
meetingNotesproperty of ourMeetingNotesWorkflow.
Step 3: Create the MeetingNotesDocument
Before we can run the workflow we need to define the MeetingNotesDocument.
Create the file
src/example-module/tutorials/meeting-notes-example/documents/meeting-notes-document.ts:
import { z } from 'zod';
import { BlockConfig } from '@loopstack/shared';
import { Document } from '@loopstack/core';
import { Expose } from 'class-transformer';
@BlockConfig({
properties: z.object({
text: z.string(),
}),
configFile: __dirname + '/meeting-notes-document.yaml',
})
export class MeetingNotesDocument extends Document {
@Expose()
text: string;
}The MeetingNotesDocument contains the state of the document which in this case is only one property text. We define the document validation in the BlockConfig which makes sure
that the document has the correct content when it is created. Finally, we define the document config in the configFile.
Step 4: Create the MeetingNotesDocument Configuration
Create the file
src/example-module/tutorials/meeting-notes-example/documents/meeting-notes-document.yaml:
ui:
properties:
text:
title: Notes
widget: textarea-expand
buttons:
- transition: user_response
label: "Optimize Notes"In this file:
- we define that the text property of the document should be displayed in a textarea widget.
- we add a button to the document that will trigger the transition
user_responseof our workflow alongside the form data as payload.
Step 5: Add the workflow
Add the workflow to your workspace and add workflow and document as providers of the module.
src/example-module/example-workspace.ts
import { BlockConfig } from '@loopstack/shared';
import { Workspace } from '@loopstack/core';
import { MeetingNotesWorkflow } from './tutorials/meeting-notes-example/meeting-notes.workflow';
@BlockConfig({
imports: [
// other workflows
MeetingNotesWorkflow,
],
config: {
title: 'Examples Workspace'
},
})
export class ExampleWorkspace extends Workspace {}src/example-module/example.module.ts
import { Module } from '@nestjs/common';
import { CoreToolsModule, LoopCoreModule } from '@loopstack/core';
import { ModuleFactory } from '@loopstack/shared';
import { LlmModule } from '@loopstack/llm';
import { ExampleWorkspace } from './example-workspace';
import { ExampleModuleFactoryService } from './example-module-factory.service';
import { MeetingNotesWorkflow } from './tutorials/meeting-notes-example/meeting-notes.workflow';
import { MeetingNotesDocument } from './tutorials/meeting-notes-example/documents/meeting-notes-document';
@Module({
imports: [
LoopCoreModule, // import core module
CoreToolsModule, // import built-in core tools
LlmModule // import llm tools module
],
providers: [
ExampleWorkspace,
// other
MeetingNotesWorkflow, // add custom workflow and document
MeetingNotesDocument,
ExampleModuleFactoryService,
],
exports: [
ExampleModuleFactoryService
]
})
@ModuleFactory(ExampleModuleFactoryService) // make sure to define a module factory of your module
export class ExampleModule {}Step 6: Run the workflow
We can now run the first version of our workflow.
- Log-in to the Loopstack Studio at http://app.loopstack.aiΒ .
- Select your local environment and go to
Workspaces - Create a new
ExamplesWorkspaceif not already done. - Click on
Runat the top right in the ExampleWorkspace and select our newly createdMeetingNotesWorkflowcalled βTutorial 1: Meeting Notesβ
You will see:
- a form with just a textarea called
Meeting Notesis displayed. Click on the ButtonOptimize Notes. - The form will re-render and the button will disappear. We now have updated the
MeetingNotesDocumentand can continue processing.
Step 7: Finalize the Workflow configuration
Now that the first part of the workflow is done, lets implement the actual processing with the use of a llm.
Update the file
src/example-module/tutorials/meeting-notes-example/meeting-notes.workflow.yaml:
transitions:
- id: create_form
from: start
to: waiting_for_response
call:
- id: form
tool: CreateDocument
args:
document: MeetingNotesDocument
update:
content:
text: |
meeting 1.1.2025
budget: need 2 cut costs sarah said
hire new person?? --> marketing
vendor pricing - follow up needed by anna
mike's project delayed again. deadline moved - when??? next month maybe
client wants faster delivery but budget tight. can we do it?
- id: user_response
from: waiting_for_response
to: response_received
when: manual
call:
- id: create_response
tool: CreateDocument
args:
document: MeetingNotesDocument
update:
content: ${ ctx.payload.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
responseDocument: 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
when: manual
call:
- id: create_response
tool: CreateDocument
args:
document: OptimizedNotesDocument
update:
content: ${ ctx.payload.transition.payload }
assign:
optimizedNotes: ${ result.data.content }What we did here:
- we added the
optimize_notestransition which is executed when the user clicked the button and the workflow transitioned to the placeresponse_received. - In this transition we execute the
AiGenerateDocumenttool which is part of thellm moduleof Loopstack. - We use the model
gpt-4ofromopenaito generate ourOptimizedNotesDocumentusing a prompt we provided including the originalmeetingNotestext in the context. - The AI generated document is stored in the
optimizedNotesproperty of our workflow, so we can access it in other parts of the workflow. - finally, we add another manual transition
confirmwhich again, waits for the user to edit and confirm the generated document.
Step 8: Create the OptimizedNotesDocument
Now there is one more document we need to define: OptimizedNotesDocument
Create the file
src/example-module/tutorials/meeting-notes-example/documents/optimized-notes-document.ts:
import { z } from 'zod';
import { BlockConfig } from '@loopstack/shared';
import { Document } from '@loopstack/core';
import { Expose } from 'class-transformer';
@BlockConfig({
properties: z.object({
date: z.string(),
summary: z.string(),
participants: z.array(z.string()),
decisions: z.array(z.string()),
actionItems: z.array(z.string()),
}),
configFile: __dirname + '/optimized-notes-document.yaml',
})
export class OptimizedNotesDocument extends Document {
@Expose()
date: string;
@Expose()
summary: string;
@Expose()
participants: string[];
@Expose()
decisions: string[];
@Expose()
actionItems: string[];
}This time the document contains multiple properties which represent the structured document we want to generate.
Create the file
src/example-module/tutorials/meeting-notes-example/documents/optimized-notes-document.yaml
ui:
order:
- date
- summary
- participants
- decisions
- actionItems
properties:
date:
title: Date
summary:
title: Summary
widget: textarea-expand
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
buttons:
- transition: confirm
label: "Confirm"Note, this time we add a button for the confirm transition. Again, we define for each property how the data should be rendered in the frontend.
Step 9: Add the Document to Module and Workflow
We now need to add the new document to the module as well as to our workflow.
src/example-module/example.module.ts
// ...
import { OptimizedNotesDocument } from './documents/optimized-notes-document';
@Module({
imports: [
LoopCoreModule,
CoreToolsModule,
LlmModule
],
providers: [
ExampleWorkspace,
// ...
MeetingNotesWorkflow,
MeetingNotesDocument,
OptimizedNotesDocument, // add the new document
ExampleModuleFactoryService,
],
exports: [
ExampleModuleFactoryService
]
})
@ModuleFactory(ExampleModuleFactoryService)
export class ExampleModule {}src/example-module/tutorials/meeting-notes-example/meeting-notes.workflow.ts:
// ...
@BlockConfig({
imports: [
AiGenerateDocument,
CreateDocument,
MeetingNotesDocument,
OptimizedNotesDocument, // add the document as import
],
config: {
title: 'Tutorial 1: Meeting Notes',
},
configFile: __dirname + '/meeting-notes.workflow.yaml',
})
export class MeetingNotesWorkflow extends Workflow {
@Input()
@Expose()
meetingNotes: MeetingNotesDocument;
@Input()
@Expose()
optimizedNotes: OptimizedNotesDocument; // add the optimizedNotes property
}Step 10: Run the workflow (again)
Letβs run our final workflow version.
- Return to the Loopstack Studio at http://app.loopstack.aiΒ .
- Select the
ExamplesWorkspace - Click on
Runand select our updatedTutorial 1: Meeting NotesWorkflow.
You will see:
- the input form just as before
- when you click on
Optimize Notesthe AI will generate our structuredOptimizedNotesDocument - it will display the final notes document and give you the chance to update and confirm the generated document.