Skip to content

4.1 Workflow Pertama

Tujuan bab

Setelah bab ini kamu bisa:

  • Membuat langkah dengan createStep() dan merangkainya dengan createWorkflow()
  • Menjelaskan kenapa .commit() wajib dan apa yang terjadi kalau lupa
  • Mendaftarkan dan mengambil workflow, serta tahu kenapa getWorkflow() lebih disarankan daripada impor langsung
  • Menjalankan workflow dengan .start() dan .stream()
  • Memeriksa hasilnya lewat result.status

Prasyarat

Dua bahan: step dan workflow

Workflow di Mastra dibangun dari dua hal saja:

  • Step — satu unit kerja, dengan schema masukan dan keluaran
  • Workflow — rangkaian step, dengan schema masukan dan keluarannya sendiri

Filosofinya persis seperti fungsi dan program: step adalah fungsi dengan kontrak yang jelas, workflow adalah cara merangkai fungsi-fungsi itu.

Membuat step

ts
// src/mastra/workflows/test-workflow.ts
import { createStep } from '@mastra/core/workflows'
import { z } from 'zod'

const step1 = createStep({
  id: 'step-1',
  inputSchema: z.object({
    message: z.string(),
  }),
  outputSchema: z.object({
    formatted: z.string(),
  }),
  execute: async ({ inputData }) => {
    const { message } = inputData

    return {
      formatted: message.toUpperCase(),
    }
  },
})

Perhatikan perbedaan halus tapi penting dari createTool() di Bagian 2:

createTool()createStep()
Parameter execute(inputData, context) — input adalah parameter pertama({ inputData }) — input adalah properti dari objek
Butuh description?Ya, dibaca modelTidak wajib — step tidak dipilih model
Dipanggil olehModelWorkflow, sesuai urutan yang kamu tentukan

Perbedaan bentuk execute ini mudah tertukar saat kamu berpindah antara menulis tool dan menulis step di hari yang sama. Kalau editor menampilkan garis merah pada destructuring-mu, ini kandidat pertama yang perlu diperiksa.

Perbedaan kedua lebih mendasar: step tidak butuh description karena tidak ada yang harus memutuskan apakah akan memanggilnya. Urutannya sudah kamu tetapkan. Ini konsekuensi langsung dari sifat deterministik workflow.

Merangkai jadi workflow

ts
// src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from '@mastra/core/workflows'
import { z } from 'zod'

const step1 = createStep({
  id: 'step-1',
  inputSchema: z.object({ message: z.string() }),
  outputSchema: z.object({ formatted: z.string() }),
  execute: async ({ inputData }) => ({
    formatted: inputData.message.toUpperCase(),
  }),
})

const step2 = createStep({
  id: 'step-2',
  inputSchema: z.object({ formatted: z.string() }),
  outputSchema: z.object({ emphasized: z.string() }),
  execute: async ({ inputData }) => ({
    emphasized: `${inputData.formatted}!!!`,
  }),
})

export const testWorkflow = createWorkflow({
  id: 'test-workflow',
  inputSchema: z.object({ message: z.string() }),
  outputSchema: z.object({ emphasized: z.string() }),
})
  .then(step1)
  .then(step2)
  .commit()

Tiga hal yang perlu diperhatikan.

Schema harus bersambung. outputSchema dari step1 adalah { formatted: string }, dan itu persis inputSchema dari step2. Kalau tidak cocok, TypeScript akan mengeluh — di editor, sebelum kamu menjalankan apa pun. Inilah manfaat konkret dari Mastra yang dirancang untuk TypeScript.

.commit() wajib. Ia menutup definisi workflow. Tanpa itu, workflow tidak selesai dibangun.

Workflow punya schema-nya sendiri. inputSchema workflow adalah apa yang diterima dari pemanggil; outputSchema adalah apa yang dikembalikan di akhir.

                 workflow.inputSchema


            ┌───────────────────────┐
            │ step-1                │
            │ in : { message }      │
            │ out: { formatted }    │
            └───────────┬───────────┘
                        │  harus cocok
            ┌───────────▼───────────┐
            │ step-2                │
            │ in : { formatted }    │
            │ out: { emphasized }   │
            └───────────┬───────────┘

                workflow.outputSchema

Mendaftarkan

Seperti agent, workflow perlu didaftarkan:

ts
// src/mastra/index.ts
import { Mastra } from '@mastra/core/mastra'
import { testWorkflow } from './workflows/test-workflow.ts'

export const mastra = new Mastra({
  workflows: { testWorkflow },
})

Setelah terdaftar, workflow bisa dipanggil dari agent, dari tool, dari Mastra Client, atau dari baris perintah — dan ia mendapat akses ke sumber daya bersama seperti logging dan observability.

Mengambil referensinya

ts
const testWorkflow = mastra.getWorkflow('testWorkflow')

Dokumentasi menjelaskan kenapa cara ini lebih disarankan daripada mengimpor workflow-nya langsung, dan alasannya ada dua:

  1. Ia memberi akses ke konfigurasi instance Mastra — logger, telemetry, storage, agent yang terdaftar, dan vector store
  2. Ia memberi inferensi tipe penuh untuk schema masukan dan keluaran workflow

Perhatikan bahwa argumennya adalah registration key — key yang kamu pakai saat mendaftarkan, bukan properti id. Ada juga getWorkflowById() yang mencari berdasarkan id, tapi dokumentasi mencatat bahwa ia tidak memberi inferensi tipe sebaik getWorkflow().

Berbeda dari agent

Di 1.1 kita menyimpulkan bahwa getAgentById() yang sebaiknya dipakai untuk agent. Untuk workflow, kesimpulannya kebalikannya: getWorkflow() yang disarankan, karena inferensi tipenya lebih baik. Bedanya masuk akal — schema workflow jauh lebih berperan dalam kode yang kamu tulis daripada schema agent.

Menjalankan workflow

Ada dua mode, dan keduanya dimulai dengan membuat run.

.start() — tunggu sampai selesai

ts
// run.mjs
import { mastra } from './src/mastra/index.ts'

const testWorkflow = mastra.getWorkflow('testWorkflow')
const run = await testWorkflow.createRun()

const result = await run.start({
  inputData: {
    message: 'Hello world',
  },
})

if (result.status === 'success') {
  console.log(result.result)
}

Perhatikan pola tiga langkah: ambil workflow → buat run → jalankan run. Objek run mewakili satu eksekusi tertentu, dan inilah yang membuat suspend/resume di 4.5 mungkin — kamu bisa memegang satu run, meninggalkannya, lalu kembali kepadanya.

result.status harus diperiksa. Sebuah workflow bisa berakhir dengan beberapa keadaan berbeda, dan 'success' hanya salah satunya. Ada juga keadaan gagal, dan keadaan tertunda saat sebuah langkah menunggu masukan. Langsung membaca result.result tanpa memeriksa status adalah cara cepat mendapat nilai undefined yang membingungkan.

.stream() — pantau saat berjalan

ts
// run.mjs
import { mastra } from './src/mastra/index.ts'

const testWorkflow = mastra.getWorkflow('testWorkflow')
const run = await testWorkflow.createRun()

const stream = run.stream({
  inputData: { message: 'Hello world' },
})

for await (const event of stream.fullStream) {
  console.log(event)
}

const result = await stream.result
console.log(result)

Berbeda dari stream() pada agent yang memancarkan potongan teks, stream workflow memancarkan peristiwa terstruktur yang menggambarkan siklus hidup eksekusi — langkah mana yang mulai, langkah mana yang selesai.

Ini yang kamu pakai untuk menampilkan progres: "Langkah 2 dari 5: memvalidasi data…". Untuk workflow yang berjalan lama, ini perbedaan besar dari sisi pengalaman pengguna.

Workflow di dalam workflow

Sebuah workflow bisa dipakai sebagai step di workflow lain:

ts
const childWorkflow = createWorkflow({
  id: 'child-workflow',
  inputSchema: z.object({ message: z.string() }),
  outputSchema: z.object({ emphasized: z.string() }),
})
  .then(step1)
  .then(step2)
  .commit()

export const parentWorkflow = createWorkflow({
  id: 'parent-workflow',
  inputSchema: z.object({ message: z.string() }),
  outputSchema: z.object({ emphasized: z.string() }),
})
  .then(childWorkflow)
  .commit()

Aturan schema-nya sama persis seperti step biasa. Ini cara utama memecah workflow besar menjadi bagian yang bisa dipahami — dan dipakai ulang.

Kalau kamu ingin memakai ulang logika workflow tapi melacaknya secara terpisah, ada cloneWorkflow():

ts
import { cloneWorkflow } from '@mastra/core/workflows'

const clonedWorkflow = cloneWorkflow(parentWorkflow, { id: 'cloned-workflow' })

Tiap klon berjalan independen dan muncul sebagai workflow berbeda di log dan perkakas observability.

Kesalahan umum

Gejala: Error tipe pada .then(step2), dengan pesan tentang ketidakcocokan tipe yang panjang dan sulit dibaca. Penyebab: outputSchema step sebelumnya tidak cocok dengan inputSchema step berikutnya. Sering karena beda nama field yang sepele — formatted versus formattedText. Perbaikan: Baca bagian akhir pesan error, di mana biasanya disebutkan properti mana yang hilang atau berlebih. Samakan nama field, atau sisipkan .map() untuk mengubah bentuk data di antara keduanya — caranya di 4.2.

Gejala: result.result bernilai undefined, padahal workflow terlihat berjalan tanpa error. Penyebab: Status eksekusi bukan 'success'. Bisa karena sebuah langkah gagal, atau karena workflow tertunda menunggu masukan. Perbaikan: Periksa result.status sebelum membaca result.result. Jadikan pemeriksaan status sebagai kebiasaan tetap, bukan sesuatu yang ditambahkan setelah bermasalah.

Coba sendiri

Tantangan: Bangun invoice-workflow yang memproses data faktur sederhana melalui tiga langkah berurutan.

Langkah yang diminta:

  1. normalisasi — menerima nama pelanggan dan daftar item (nama, harga, jumlah); merapikan nama pelanggan dan memastikan angka bertipe number
  2. hitung — menghitung subtotal, PPN 11%, dan total
  3. format — menghasilkan ringkasan teks yang siap dikirim

Ketentuan:

  1. Ketiga langkah punya inputSchema dan outputSchema yang eksplisit
  2. Schema harus bersambung tanpa perlu .map()
  3. Daftarkan ke instance Mastra, ambil dengan getWorkflow()
  4. Jalankan dengan .start(), periksa result.status sebelum membaca hasil
  5. Buat versi kedua yang memakai .stream() dan mencetak setiap peristiwa
  6. Sengaja rusak sambungan schema — ubah satu nama field di outputSchema langkah kedua — dan amati pesan error yang muncul di editor

Checklist penerimaan:

  • [ ] Workflow berjalan dan menghasilkan ringkasan yang benar
  • [ ] result.status diperiksa sebelum result.result diakses
  • [ ] Versi .stream() mencetak peristiwa untuk ketiga langkah
  • [ ] Setelah langkah 6, kamu bisa menunjukkan di mana editor menandai kesalahan — sebelum menjalankan apa pun
  • [ ] Ketiga langkah muncul terpisah di Studio saat workflow dijalankan

Petunjuk: Langkah 6 adalah inti latihan ini. Banyak orang melewatkannya karena terasa seperti sengaja membuat masalah — padahal justru di situ nilai workflow bertipe terlihat. Perhatikan bahwa kesalahannya muncul di baris .then(), bukan di dalam execute, dan itu memberitahu banyak tentang di mana Mastra memverifikasi sambungan antar langkah.

Ikhtisar

  • Workflow dibangun dari step (createStep) yang dirangkai dengan createWorkflow, dan ditutup dengan .commit().
  • execute pada step menerima objek({ inputData }) — berbeda dari tool yang menerima input sebagai parameter pertama.
  • Step tidak butuh description karena bukan model yang memilihnya.
  • Schema antar langkah harus bersambung, dan TypeScript memverifikasinya di editor sebelum kamu menjalankan apa pun.
  • Daftarkan di new Mastra({ workflows }), lalu ambil dengan getWorkflow() memakai registration key — ia memberi inferensi tipe yang lebih baik daripada getWorkflowById().
  • Pola menjalankan: ambil workflow → createRun().start() atau .stream(). Objek run inilah yang memungkinkan suspend dan resume.
  • Selalu periksa result.status sebelum membaca result.result.
  • Workflow bisa menjadi step di workflow lain; cloneWorkflow() memakai ulang logikanya dengan identitas terpisah.

Lanjut ke mana

Sejauh ini langkah-langkah hanya berbaris lurus. Bab berikutnya membuka semua bentuk alur lainnya — paralel, bercabang, dan berulang: 4.2 Control Flow.

Materi belajar mandiri. Bukan dokumentasi resmi Mastra.