Tampilan
4.3 Workflow State
Tujuan bab
Setelah bab ini kamu bisa:
- Menjelaskan perbedaan antara state dan aliran input/output antar langkah
- Mendefinisikan
stateSchemadi tingkat workflow maupun tingkat langkah - Membaca dan memperbarui state dengan
statedansetState - Menetapkan nilai awal lewat
initialState - Memutuskan kapan sebuah nilai layak masuk state, dan kapan sebaiknya tetap lewat schema langkah
Prasyarat
Masalah mengoper data lewat setiap langkah
Bayangkan workflow tujuh langkah. Langkah pertama menghitung ID transaksi, dan langkah ketujuh membutuhkannya untuk mencatat hasil akhir.
Dengan hanya mengandalkan aliran input/output, ID itu harus ikut menumpang di outputSchema langkah 1, lalu di inputSchema dan outputSchema langkah 2, 3, 4, 5, dan 6 — meskipun tak satu pun dari mereka memakainya.
❌ ID menumpang di seluruh rantai
step1 → { hasil, transaksiId }
step2 → { hasil, transaksiId } ← tidak memakainya
step3 → { hasil, transaksiId } ← tidak memakainya
...
step7 → memakai transaksiIdHasilnya: schema yang penuh field tidak relevan, dan langkah-langkah yang kehilangan sifat mandirinya. Menghapus langkah 4 sekarang merusak langkah 7.
Workflow state menyelesaikan ini dengan menyediakan penyimpanan bersama yang bisa dibaca dan ditulis semua langkah, tanpa harus melewati schema masing-masing.
✅ ID disimpan di state
┌──────────── state: { transaksiId } ────────────┐
│ │
step1 → step2 → step3 → step4 → step5 → step6 → step7
tulis bacaState versus input/output
Dokumentasi membedakan keduanya dengan tegas, dan pembedaan ini layak dihafal:
| Input/output langkah | State | |
|---|---|---|
| Arah aliran | Berurutan, satu langkah ke langkah berikutnya | Bersama, semua langkah bisa baca-tulis |
| Cakupan | Antara dua langkah bertetangga | Seluruh eksekusi workflow |
| Diakses lewat | inputData dan nilai kembalian | state dan setState |
| Bertahan saat suspend/resume | Melalui snapshot | Ya, ikut bertahan |
Poin terakhir penting dan akan terpakai di 4.5: state bertahan melewati siklus berhenti dan lanjut.
Bentuk dasarnya
ts
// src/mastra/workflows/state-workflow.ts
import { createStep } from '@mastra/core/workflows'
import { z } from 'zod'
const step1 = createStep({
id: 'step-1',
inputSchema: z.object({ workflowInput: z.string() }),
outputSchema: z.object({ step1Output: z.string() }),
stateSchema: z.object({ sharedCounter: z.number() }),
execute: async ({ inputData, state, setState }) => {
// inputData berasal dari input workflow atau keluaran langkah sebelumnya
console.log(inputData.workflowInput)
// state adalah penyimpanan bersama seluruh workflow
console.log(state.sharedCounter)
// Perbarui state untuk langkah-langkah berikutnya
await setState({ sharedCounter: state.sharedCounter + 1 })
// Nilai kembalian mengalir ke inputData langkah berikutnya
return { step1Output: 'processed' }
},
})Tiga hal baru dibanding step biasa:
stateSchemapada definisi step — mendeklarasikan bagian state yang dipakai step inistatepada parameterexecute— untuk membacasetStatepada parameterexecute— untuk menulis, dan perlu di-await
Schema induk dan bagiannya
Ini pola yang membuat state tetap terkendali di workflow besar.
Workflow mendeklarasikan schema state lengkap; tiap langkah mendeklarasikan hanya bagian yang ia butuhkan.
ts
// src/mastra/workflows/state-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() }),
stateSchema: z.object({
processedItems: z.array(z.string()),
}),
execute: async ({ inputData, state, setState }) => {
const { message } = inputData
const { processedItems } = state
await setState({
processedItems: [...processedItems, 'item-1', 'item-2'],
})
return { formatted: message.toUpperCase() }
},
})
const step2 = createStep({
id: 'step-2',
inputSchema: z.object({ formatted: z.string() }),
outputSchema: z.object({ emphasized: z.string() }),
stateSchema: z.object({
metadata: z.object({
processedBy: z.string(),
}),
}),
execute: async ({ inputData, state }) => {
const { formatted } = inputData
const { metadata } = state
return { emphasized: `${formatted}!! ${metadata.processedBy}` }
},
})
export const stateWorkflow = createWorkflow({
id: 'state-workflow',
inputSchema: z.object({ message: z.string() }),
outputSchema: z.object({ emphasized: z.string() }),
stateSchema: z.object({
processedItems: z.array(z.string()),
metadata: z.object({
processedBy: z.string(),
}),
}),
})
.then(step1)
.then(step2)
.commit()Perhatikan bahwa step1 hanya mendeklarasikan processedItems dan step2 hanya metadata — meskipun workflow mendeklarasikan keduanya. Ini bukan sekadar kerapian: schema langkah menjadi dokumentasi tentang apa yang disentuh langkah itu. Saat kamu membaca step2 enam bulan kemudian, kamu langsung tahu ia tidak akan mengubah processedItems.
Nilai awal
State harus punya nilai awal, dan itu diberikan saat memulai run:
ts
// run.mjs
import { mastra } from './src/mastra/index.ts'
const workflow = mastra.getWorkflow('stateWorkflow')
const run = await workflow.createRun()
const result = await run.start({
inputData: { message: 'halo' },
initialState: {
processedItems: [],
metadata: { processedBy: 'sistem-batch' },
},
})initialState diberikan berdampingan dengan inputData, dan bentuknya harus sesuai stateSchema workflow.
Pola yang berguna: initialState adalah tempat yang tepat untuk konfigurasi seluruh workflow — mode operasi, identitas pemanggil, feature flag. Nilai seperti itu relevan untuk banyak langkah tapi bukan bagian dari data yang sedang diproses.
Kapan memakai state, kapan tidak
State memudahkan, dan karena itu mudah disalahgunakan. Workflow yang seluruh datanya lewat state kehilangan hal yang membuat workflow bertipe itu berharga: sambungan antar langkah yang diverifikasi compiler.
| Pakai state untuk | Tetap pakai input/output untuk |
|---|---|
| Konfigurasi seluruh workflow | Data yang sedang diproses |
| Akumulasi hasil dari beberapa langkah | Keluaran satu langkah untuk langkah berikutnya |
| Pelacakan progres | Apa pun yang dipakai hanya oleh langkah tetangga |
| Nilai yang dipakai langkah berjauhan | — |
Pertanyaan pemutus: apakah nilai ini dipakai oleh langkah yang tidak bertetangga? Kalau tidak, biarkan lewat input/output — sambungan yang diverifikasi tipe lebih berharga daripada kenyamanan sesaat.
Kesalahan umum
Gejala: Nilai state yang diperbarui di satu langkah terbaca sebagai nilai lama di langkah berikutnya. Penyebab: setState tidak di-await. Ia mengembalikan promise, dan tanpa await langkah bisa selesai sebelum pembaruan benar-benar tersimpan. Perbaikan: Selalu tulis await setState({ ... }).
Gejala: Error tipe saat mengakses sebuah field dari state di dalam sebuah langkah, padahal field itu jelas ada di stateSchema workflow. Penyebab: Langkah tersebut tidak mendeklarasikan field itu di stateSchema miliknya sendiri. Tiap langkah hanya melihat bagian state yang ia deklarasikan. Perbaikan: Tambahkan field yang dibutuhkan ke stateSchema langkah itu. Kalau kamu mendapati diri menambahkan hampir semua field ke hampir semua langkah, pertimbangkan apakah data itu memang seharusnya lewat state.
Coba sendiri
Tantangan: Bangun audit-workflow yang memproses berkas unggahan dan mengumpulkan jejak audit sepanjang jalan.
Alur yang diminta — lima langkah berurutan: validasi format, pindai keamanan, ekstrak metadata, kompresi, dan simpan.
Ketentuan:
- Definisikan
stateSchemadi tingkat workflow yang memuat:jejakAudit(array string),mulaiPada(string), danmode(enum'cepat'atau'teliti') - Setiap langkah menambahkan satu entri ke
jejakAudit— tapi hanya mendeklarasikan bagian state yang benar-benar ia pakai - Langkah "pindai keamanan" dan "kompresi" membaca
modedan berperilaku berbeda untuk'cepat'dan'teliti' mulaiPadadanmodediberikan lewatinitialState, bukan lewatinputData- Langkah terakhir mengembalikan seluruh
jejakAuditsebagai bagian dari keluaran workflow - Jalankan dua kali dengan
modeberbeda dan bandingkan jejaknya
Checklist penerimaan:
- [ ]
jejakAuditdi akhir berisi lima entri, satu dari tiap langkah, dengan urutan yang benar - [ ] Tidak ada satu pun langkah yang mendeklarasikan field state yang tidak ia pakai
- [ ] Setiap
setStatedi-await - [ ] Kedua mode menghasilkan jejak yang berbeda pada dua langkah yang relevan
- [ ]
modedanmulaiPadatidak muncul diinputSchemasatu pun langkah - [ ] Kamu bisa menjelaskan kenapa
jejakAuditlayak di state, sementara metadata hasil ekstraksi sebaiknya lewat input/output
Petunjuk: Checklist terakhir menguji pemahaman inti. jejakAudit dikumpulkan oleh semua langkah dan dipakai di akhir — itu persis kasus untuk state. Metadata hasil ekstraksi hanya diteruskan dari langkah ekstraksi ke langkah kompresi yang bertetangga — sambungan langsung lebih tepat, dan compiler akan memverifikasinya untukmu.
Ikhtisar
- State adalah penyimpanan bersama yang bisa dibaca dan ditulis semua langkah, tanpa harus menumpang di schema tiap langkah di antaranya.
- Diakses lewat
stateuntuk membaca dansetStateuntuk menulis;setStateharus di-await. - Workflow mendeklarasikan
stateSchemalengkap; tiap langkah mendeklarasikan hanya bagian yang ia butuhkan — dan itu berfungsi sebagai dokumentasi tentang apa yang disentuh langkah tersebut. - Nilai awal diberikan lewat
initialStatesaat memulai run, berdampingan denganinputData. - State bertahan melewati suspend dan resume.
- Pakai state untuk konfigurasi, akumulasi, dan nilai yang dipakai langkah berjauhan. Data yang mengalir antar langkah bertetangga tetap lewat input/output, supaya sambungannya diverifikasi compiler.
Lanjut ke mana
Semua langkah sejauh ini berisi logika biasa. Bab berikutnya memasukkan kembali kecerdasan ke dalam alur yang deterministik — memanggil agent dan tool dari dalam workflow: 4.4 Agent & Tool di Workflow.