Skip to Content
DocumentationGetting Started🏁 Your First Automation

🏁 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:

  1. Displays a form for the user to input casual meeting notes
  2. Waits for user input and captures their response
  3. Uses a LLM to transform the notes into a professional summary
  4. 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-4o model. You’ll need to add your OpenAI API key to your project’s .env file

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.yaml

To complete this tutorial:

  • Delete or rename the meeting-notes-example directory, 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 MeetingNotesWorkflow which extends the Workflow class.
  • we import several Tools and Documents in the BlockConfig that are needed in the workflow.
  • we give the workflow the title β€˜Tutorial 1: Meeting Notes’.
  • we define the configFile where we will define the exact workflow that will be executed.
  • we define the property meetingNotes which contain the original notes the user defines.
  • we decorated the meetingNotes property with @Input() and @Expose(). @Input() allows setting this property from within the workflow run using the assign method. @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) to waiting_for_response. In this transition we create the MeetingNotesDocument and populate it with a default value.
  • next, we define the second transition from waiting_for_response to the place response_received. It is a manual transition defined with the property when which makes the state machine pause the execution here until the user sends a response.
  • we update the MeetingNotesDocument by 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 meetingNotes property of our MeetingNotesWorkflow.

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_response of 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.

  1. Log-in to the Loopstack Studio at http://app.loopstack.aiΒ .
  2. Select your local environment and go to Workspaces
  3. Create a new ExamplesWorkspace if not already done.
  4. Click on Run at the top right in the ExampleWorkspace and select our newly created MeetingNotesWorkflow called β€˜Tutorial 1: Meeting Notes’

You will see:

  • a form with just a textarea called Meeting Notes is displayed. Click on the Button Optimize Notes.
  • The form will re-render and the button will disappear. We now have updated the MeetingNotesDocument and 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_notes transition which is executed when the user clicked the button and the workflow transitioned to the place response_received.
  • In this transition we execute the AiGenerateDocument tool which is part of the llm module of Loopstack.
  • We use the model gpt-4o from openai to generate our OptimizedNotesDocument using a prompt we provided including the original meetingNotes text in the context.
  • The AI generated document is stored in the optimizedNotes property of our workflow, so we can access it in other parts of the workflow.
  • finally, we add another manual transition confirm which 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.

  1. Return to the Loopstack Studio at http://app.loopstack.aiΒ .
  2. Select the ExamplesWorkspace
  3. Click on Run and select our updated Tutorial 1: Meeting Notes Workflow.

You will see:

  • the input form just as before
  • when you click on Optimize Notes the AI will generate our structured OptimizedNotesDocument
  • it will display the final notes document and give you the chance to update and confirm the generated document.
Last updated on