Tampilan
4.1 Workflow Pertama
Tujuan bab
Setelah bab ini kamu bisa:
- Membuat langkah dengan
createStep()dan merangkainya dengancreateWorkflow() - 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
- Bagian 3 selesai
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 model | Tidak wajib — step tidak dipilih model |
| Dipanggil oleh | Model | Workflow, 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.outputSchemaMendaftarkan
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:
- Ia memberi akses ke konfigurasi instance Mastra — logger, telemetry, storage, agent yang terdaftar, dan vector store
- 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:
normalisasi— menerima nama pelanggan dan daftar item (nama, harga, jumlah); merapikan nama pelanggan dan memastikan angka bertipe numberhitung— menghitung subtotal, PPN 11%, dan totalformat— menghasilkan ringkasan teks yang siap dikirim
Ketentuan:
- Ketiga langkah punya
inputSchemadanoutputSchemayang eksplisit - Schema harus bersambung tanpa perlu
.map() - Daftarkan ke instance Mastra, ambil dengan
getWorkflow() - Jalankan dengan
.start(), periksaresult.statussebelum membaca hasil - Buat versi kedua yang memakai
.stream()dan mencetak setiap peristiwa - Sengaja rusak sambungan schema — ubah satu nama field di
outputSchemalangkah kedua — dan amati pesan error yang muncul di editor
Checklist penerimaan:
- [ ] Workflow berjalan dan menghasilkan ringkasan yang benar
- [ ]
result.statusdiperiksa sebelumresult.resultdiakses - [ ] 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 dengancreateWorkflow, dan ditutup dengan.commit(). executepada step menerima objek —({ inputData })— berbeda dari tool yang menerima input sebagai parameter pertama.- Step tidak butuh
descriptionkarena 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 dengangetWorkflow()memakai registration key — ia memberi inferensi tipe yang lebih baik daripadagetWorkflowById(). - Pola menjalankan: ambil workflow →
createRun()→.start()atau.stream(). Objek run inilah yang memungkinkan suspend dan resume. - Selalu periksa
result.statussebelum membacaresult.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.