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

Implementing 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 MeetingNotesDocumentSchema as a Zod schema so it can be reused in the workflow’s @WithState decorator
  • We use the @Injectable() decorator (from NestJS) to make the document injectable
  • We use @BlockConfig to specify the YAML configuration file
  • We use @WithArguments to define the document’s properties schema (just a text string in this case)
  • The class extends DocumentBase from @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: document to identify this as a document configuration
  • We define the UI form under ui.form.properties, configuring the text property to display as a textarea widget
  • We add an action button under ui.actions that triggers the user_response transition 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 MeetingNotesWorkflow which extends the WorkflowBase class
  • We use @BlockConfig to define the configFile where the workflow transitions are defined
  • We use @WithArguments to define the input arguments the workflow accepts (in this case, inputText)
  • We use @WithState to define the workflow state schema, which includes the meetingNotes property
  • We use the @Tool() decorator to inject the CreateDocument tool for creating/updating documents
  • We use the @Document() decorator to inject the MeetingNotesDocument we 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 title and description for the workflow
  • We define a ui.form section that configures how workflow arguments are displayed (the inputText field as a textarea)
  • We define the first transition create_form from start to waiting_for_response. In this transition we create the meetingNotesDocument and populate it with the input text using template syntax {{ args.inputText }}
  • We define the second transition user_response from waiting_for_response to response_received. It uses trigger: manual which 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 the meetingNotes state 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: LoopCoreModule and CoreUiModule (for the CreateDocument tool)
  • We register the workflow and the MeetingNotesDocument as providers
  • 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 MeetingNotesWorkflow class
  • 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 imports array 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.

  1. Open the Loopstack Studio at http://localhost:3000Β  (or where you are running it)
  2. Go to Workspaces
  3. Open or create a new β€œDefault Workspace”
  4. Click on Run at the top right and select our newly created MeetingNotesWorkflow called β€˜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, and actionItems
  • 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 order of fields in the form
  • We configure each property’s display settings (title, widget type, collapsed state for arrays)
  • Array properties use the items configuration to define how each item is displayed
  • We add a button action that triggers the confirm transition

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 OptimizedNotesDocument and its schema
  • We imported the AiGenerateDocument tool from @loopstack/ai-module
  • We added optimizedNotes to the workflow state
  • We added the @Tool() decorator for aiGenerateDocument
  • We added the @Document() decorator for optimizedNotesDocument

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 AiModule for the AiGenerateDocument tool
  • We added AiModule to the imports array
  • We added OptimizedNotesDocument to the providers array

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_notes transition which executes when the user clicks the button and the workflow transitions to response_received
  • In this transition we call the aiGenerateDocument tool from the AI module
  • We configure the LLM to use gpt-4o from openai
  • We specify the response configuration with an id and the target document type (optimizedNotesDocument)
  • We provide a prompt that includes the original meetingNotes.text using template syntax
  • The AI-generated document is assigned to the optimizedNotes state property
  • The confirm transition with trigger: manual waits for the user to review and confirm the generated document

Step 14: Run the workflow (again)

Let’s run our final workflow version.

  1. Return to the Loopstack Studio
  2. Open the β€œDefault Workspace”
  3. Click on Run and select our updated workflow

You will see:

  • The input form with the textarea
  • When you click Optimize Notes, the AI will generate the structured OptimizedNotesDocument
  • The final notes document is displayed with fields for date, summary, participants, decisions, and action items
  • You can edit any field and click Confirm to finalize the document
Last updated on