Skip to Content
DocumentationGetting StartedYour 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 { z } from 'zod'; import { Document, DocumentInterface, Input } from '@loopstack/common'; export const MeetingNotesDocumentSchema = z.object({ text: z.string(), }); @Document({ configFile: __dirname + '/meeting-notes-document.yaml', }) export class MeetingNotesDocument implements DocumentInterface { @Input({ schema: MeetingNotesDocumentSchema, }) content: { text: string; }; }

In this file:

  • We export the MeetingNotesDocumentSchema as a Zod schema so it can be reused in the workflow’s @State decorator
  • We use @Document to specify the YAML configuration file
  • We implement DocumentInterface and use @Input with the schema to define the document’s content property (just a text string in this case)

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 { z } from 'zod'; import { InjectDocument, InjectTool, Input, Runtime, State, Workflow } from '@loopstack/common'; import { CreateDocument } from '@loopstack/core-ui-module'; import { MeetingNotesDocument, MeetingNotesDocumentSchema } from './documents/meeting-notes-document'; @Workflow({ configFile: __dirname + '/meeting-notes.workflow.yaml', }) export class MeetingNotesWorkflow { @InjectTool() createDocument: CreateDocument; @InjectDocument() meetingNotesDocument: MeetingNotesDocument; @Input({ schema: z.object({ inputText: z .string() .default( '- meeting 1.1.2025\n- budget: need 2 cut costs sarah said\n- hire new person?? --> marketing\n- vendor pricing - follow up needed by anna', ), }), }) args: { inputText: string; }; @State({ schema: z.object({ meetingNotes: MeetingNotesDocumentSchema.optional(), }), }) state: { meetingNotes?: z.infer<typeof MeetingNotesDocumentSchema>; }; @Runtime() runtime: any; }

In this file:

  • We use @Workflow to define the configFile where the workflow transitions are defined
  • We use @Input to define the input arguments the workflow accepts (in this case, inputText with a default value)
  • We use @State to define the workflow state schema, which includes the meetingNotes property
  • We use @Runtime() to inject the workflow runtime context
  • We use the @InjectTool() decorator to inject the CreateDocument tool for creating/updating documents
  • We use the @InjectDocument() 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: ${{ runtime.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 ${{ runtime.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 { CoreUiModule } from '@loopstack/core-ui-module'; import { MeetingNotesWorkflow } from './meeting-notes.workflow'; import { MeetingNotesDocument } from './documents/meeting-notes-document'; @Module({ imports: [CoreUiModule], providers: [ MeetingNotesWorkflow, MeetingNotesDocument, ], exports: [ MeetingNotesWorkflow, ] }) export class MeetingNotesExampleModule {}

In this file:

  • We import 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 { Workspace, InjectWorkflow } from '@loopstack/common'; import { HelloWorldWorkflow } from './hello-world/hello-world.workflow'; import { MeetingNotesWorkflow } from './meeting-notes-example/meeting-notes.workflow'; @Workspace({ config: { title: "Default Workspace" } }) export class DefaultWorkspace { @InjectWorkflow() helloWorld: HelloWorldWorkflow; @InjectWorkflow() meetingNotesWorkflow: MeetingNotesWorkflow; }

What we changed:

  • We imported our MeetingNotesWorkflow class
  • We added a new @InjectWorkflow() 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 { z } from 'zod'; import { Document, DocumentInterface, Input } from '@loopstack/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()), }); @Document({ configFile: __dirname + '/optimized-notes-document.yaml', }) export class OptimizedNotesDocument implements DocumentInterface { @Input({ schema: OptimizedMeetingNotesDocumentSchema, }) content: z.infer<typeof OptimizedMeetingNotesDocumentSchema>; }

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 implement DocumentInterface and use @Input with the schema to define the document’s content property
  • 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 { z } from 'zod'; import { AiGenerateDocument } from '@loopstack/ai-module'; import { InjectDocument, InjectTool, Input, Runtime, State, Workflow } from '@loopstack/common'; import { CreateDocument } from '@loopstack/core-ui-module'; import { MeetingNotesDocument, MeetingNotesDocumentSchema } from './documents/meeting-notes-document'; import { OptimizedMeetingNotesDocumentSchema, OptimizedNotesDocument } from './documents/optimized-notes-document'; @Workflow({ configFile: __dirname + '/meeting-notes.workflow.yaml', }) export class MeetingNotesWorkflow { @InjectTool() aiGenerateDocument: AiGenerateDocument; @InjectTool() createDocument: CreateDocument; @InjectDocument() meetingNotesDocument: MeetingNotesDocument; @InjectDocument() optimizedNotesDocument: OptimizedNotesDocument; @Input({ schema: z.object({ inputText: z .string() .default( '- meeting 1.1.2025\n- budget: need 2 cut costs sarah said\n- hire new person?? --> marketing\n- vendor pricing - follow up needed by anna', ), }), }) args: { inputText: string; }; @State({ schema: z.object({ meetingNotes: MeetingNotesDocumentSchema.optional(), optimizedNotes: OptimizedMeetingNotesDocumentSchema.optional(), }), }) state: { meetingNotes?: z.infer<typeof MeetingNotesDocumentSchema>; optimizedNotes?: z.infer<typeof OptimizedMeetingNotesDocumentSchema>; }; @Runtime() runtime: any; }

What we changed:

  • We imported the OptimizedNotesDocument and its schema
  • We imported the AiGenerateDocument tool from @loopstack/ai-module
  • We added optimizedNotes to the @State schema
  • We added the @InjectTool() decorator for aiGenerateDocument
  • We added the @InjectDocument() 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 { AiModule } from '@loopstack/ai-module'; import { CoreUiModule } from '@loopstack/core-ui-module'; import { MeetingNotesDocument } from './documents/meeting-notes-document'; import { OptimizedNotesDocument } from './documents/optimized-notes-document'; import { MeetingNotesWorkflow } from './meeting-notes.workflow'; @Module({ imports: [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: ${{ runtime.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> {{ state.meetingNotes.text }} </Meeting Notes> - id: confirm from: notes_optimized to: end trigger: manual call: - id: create_response tool: createDocument args: id: final document: optimizedNotesDocument update: content: ${{ runtime.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 state.meetingNotes.text using template syntax
  • 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