เบื้องหลัง AI Workflow Studio: จากไอเดียใน Portfolio สู่ระบบ Production ที่ตรวจสอบได้
บันทึกเชิงวิศวกรรมจากซอร์สจริง ว่าระบบวาด Workflow ถูกพัฒนาให้เป็นงาน Production อย่างไร ตั้งแต่สัญญาข้อมูลแบบมีเวอร์ชัน คิวที่คงทน การจัดการ Credential ไปจนถึง Timeline ที่ตรวจสอบย้อนหลังได้
- เผยแพร่
- เวลาอ่าน
- 11 นาที
โจทย์ของ AI Workflow Studio ไม่ใช่การทำ Canvas ให้ดูเหมือนระบบ Automation แต่คือการทำให้สิ่งที่ผู้ใช้วาดสามารถบันทึก รัน ป้องกันข้อมูลลับ และอธิบายผลลัพธ์ได้จริง แม้กระบวนการจะข้าม Browser, API, Worker และฐานข้อมูลหลายชั้นก็ตาม
จุดตั้งต้นไม่ใช่ Canvas แต่คือความเข้าใจระบบ
เป้าหมายแรกของ AI Workflow Studio คือทำให้ Workflow อ่านง่าย ผู้ใช้ควรมองเห็นว่าอะไรเป็นตัวเริ่ม งานแต่ละขั้นรับข้อมูลอะไร ส่งอะไรต่อ และการรันหยุดตรงไหน คุณค่าของงานชิ้นนี้ใน Portfolio จึงไม่ได้อยู่ที่ภาพกล่องต่อเส้น แต่อยู่ที่การพิสูจน์ว่าภาพนั้นเชื่อมกับระบบจริง
โครงสร้างถูกแยกเป็นสามส่วนชัดเจน แอป Next.js ของ Studio ทำหน้าที่เป็น Editor และหน้าควบคุมการทำงาน Backend ภาษา Go รับผิดชอบ Auth, Validation, Persistence, Execution และ Public Projection ส่วน Supabase Postgres เก็บ Workflow, Execution, Stage, Credential และ Audit Log ขณะที่เว็บ Portfolio เป็นอีกแอปหนึ่งซึ่งอ่านบทความสองภาษาจาก Backend ไม่ได้อยู่ในเส้นทางการรัน Workflow
การแยกขอบเขตนี้ช่วยกันไม่ให้บทความอ้างเกินซอร์ส แม้เอกสารระดับ Ecosystem จะพูดถึง n8n, Skills และ Worker ระยะยาว แต่ Runtime ที่ตรวจสอบได้ใน Backend ปัจจุบันมีขอบเขตเฉพาะกว่า คือ Manual, Schedule และ Webhook Trigger ต่อเข้าหา Chain ของ HTTP Request ส่วน Node ชนิดอื่นมีชื่ออยู่ใน Palette ได้ แต่ Worker จะปฏิเสธด้วยสถานะ Unsupported แทนการแสดงผลสำเร็จปลอม ๆ
ยืมภาษาการใช้งานจาก n8n โดยไม่ลอกทุกฟีเจอร์
Studio ใช้ Interaction Pattern ที่คนคุ้นกับ n8n เข้าใจได้ทันที ได้แก่ Canvas แบบจุดจาก React Flow, Node ขนาดกระชับ, Palette สำหรับเพิ่มงาน, การ Rename/Delete บน Node และ Inspector ที่เปิดเมื่อเลือก Node ทั่วไปจะแสดง Parameters หรือ Settings คู่กับ Output ส่วน HTTP Request ขยายเป็นสาม Pane คือ Input, Parameters และ Output เพื่อไม่ให้ข้อมูลจากขั้นก่อนหน้าปะปนกับ Request Body
Output ชุดเดียวกันดูได้ทั้ง JSON, Table และ Schema จุดประสงค์ไม่ใช่เพิ่มลูกเล่น แต่ช่วย Debug คนละมุม ขณะเดียวกัน UI เลือกทำเฉพาะสิ่งที่ Runtime รองรับจริง HTTP Request จึงมี Method, Query, Header, Body, Response Format, Timeout, Redirect, การจัดการ Status และ Header Auth แบบจำกัดขอบเขต ส่วน Schedule รองรับ Interval, Daily, Weekly และ Cron ห้าช่องที่ผ่าน Validation พร้อม IANA Timezone และนโยบาย Misfire แบบ skip หรือ run-once
อีก Guard ที่สำคัญคือห้าม Execute ตอน Workflow ยังไม่เคย Save หรือมีการแก้ค้างอยู่ ไม่เช่นนั้นผู้ใช้จะเห็น Definition ใหม่บนจอ แต่ระบบกลับรัน Revision เก่าในฐานข้อมูล ซึ่งเป็นความผิดพลาดที่ UI ดูเหมือนปกติแต่ความหมายของผลลัพธ์ผิดตั้งแต่ต้น
เปลี่ยนจากรายชื่อ Node เป็นสัญญาข้อมูลที่มีเวอร์ชัน
โมเดลรุ่นแรกเป็นเพียง Array ของชื่อ Node เหมาะกับการทำ Summary สาธารณะ แต่ไม่พอสำหรับตำแหน่ง Edge, ตัวตนของ Trigger หรือ Config เฉพาะ Node ระบบจึงเก็บ Summary เดิมไว้เพื่อ Compatibility และเพิ่ม Definition ที่มีเวอร์ชันข้างกัน
WorkflowDefinitionV1 version: 1 nodes: [{ id, type, kind, label, position, config }] edges: [{ id, source, target }]
ฝั่ง TypeScript ตรวจ ID ซ้ำ, Node Type ที่ไม่รู้จัก, คู่ Type/Kind ที่ผิด, Position ที่ใช้ไม่ได้, Self-edge, Edge ID ซ้ำ และ Edge ที่ชี้ไปยัง Node ที่ไม่มีจริง ฝั่ง Go ตรวจซ้ำอีกครั้งก่อน Compile เพราะข้อมูลจาก Browser ไม่ควรถูกถือเป็นคำสั่งที่เชื่อถือได้ Config อยู่ใน Node ตามชนิดของมัน ส่วน Credential เก็บเพียง ID อ้างอิง ไม่ฝังคีย์ลงใน JSON กลาง
React Flow เป็นเจ้าของ State ที่ผู้ใช้เห็น การเพิ่ม เปลี่ยนชื่อ ลาก แก้ Config หรือลบ Node จะถูกแปลงกลับเป็น Definition แล้วส่งให้ Editor Shell เตรียม Save ทุกครั้ง Edge ไม่ได้เปิดให้วาดหรือลบอิสระ แต่ถูก Derive ใหม่จากกฎของระบบและตั้งเป็น Non-selectable/Non-deletable ข้อจำกัดนี้ทำให้ Canvas ที่ลากได้ยังคงตรงกับ Runtime ที่ตั้งใจให้แคบและตรวจสอบได้
Schedule กับ Manual คือสองราก ไม่ใช่สองขั้นต่อกัน
Default Workflow แสดงกฎสำคัญที่สุดของ Graph: Schedule และ Manual Trigger เป็น Root แยกกัน ทั้งคู่เชื่อมเข้าหา Action ตัวแรก แล้วใช้ Chain ปลายทางร่วมกัน การรันตามเวลาไม่ควรต้องผ่าน Manual Trigger และการกด Manual ก็ไม่ควรแก้ความหมายของ Schedule
Edge Builder จะคำนวณ Topology นี้ใหม่เมื่อ Node ถูกเพิ่ม ลบ หรือย้าย Trigger ถูกวางทางซ้ายแยกเป็นหลายแถว ส่วน Node ที่ไม่ใช่ Trigger เรียงเป็น Chain เดียวตามตำแหน่ง เมื่อถึง Runtime ตัว Graph Compiler ภาษา Go จะเลือก Trigger Root เพียงหนึ่งตัวและเดินตาม Path ที่ชัดเจนหนึ่งเส้น Compiler ปฏิเสธ Cycle, Edge ที่ย้อนเข้า Trigger, Edge ซ้ำ, Branch ที่กำกวม, Target ที่ไปไม่ถึง และ Runtime Type ที่ยังไม่รองรับ
จาก Graph เดียวกันจึงมี Execution Mode ได้สองแบบ Full Run เดินจาก Root ถึงปลายสุด ส่วน “Execute previous nodes” Compile เฉพาะ Prefix ตั้งแต่ Root ถึง Target แบบรวม Target ด้วย HTTP Inspector จึงแสดง Input และ Output จริงของ Node นั้นได้ ตรงกันข้าม ปุ่ม “Execute step” บน Trigger มีหน้าที่ทดสอบ Output ชั่วคราวของ Trigger เท่านั้น ไม่ได้หมายความว่า Node ถัดไปถูกเรียกแล้ว
Credential ต้องไม่กลายเป็นส่วนหนึ่งของ Canvas
HTTP Request ที่ใช้จริงต้องมี Auth แต่ถ้าใส่ Token ลงใน Header ของ Node โดยตรง ข้อมูลลับจะไหลไปกับ Definition, Browser State, Export, Snapshot และ Log Studio จึงแยก Header Auth ออกมาเป็น Credential Modal โดยเฉพาะ Browser ส่งชื่อ Credential, ชื่อ Header และค่าลับเฉพาะตอนสร้างหรือ Rotate เท่านั้น เมื่อ Save แล้ว UI จะเปิดดูค่าเดิมไม่ได้ หากต้องแก้ต้องกรอกทั้งชื่อ Header และ Value ใหม่
สิ่งที่ Node เก็บมีเพียง Credential ID ส่วน API รายการ Credential คืนแค่ ID, ชื่อ, Type, Status และ Timestamp Backend เข้ารหัสข้อมูลก่อนส่งไปเก็บ โดยผูกขอบเขตการถอดรหัสกับ Credential แต่ละรายการ ตารางใน Supabase เปิด Row Level Security และการ Delete เป็น Soft Revoke เพื่อเก็บประวัติอ้างอิงไว้ แต่บังคับให้การรันครั้งต่อไป Fail Closed ปุ่ม Test ตรวจว่าถอดรหัสและอ่าน Schema ได้ โดยไม่ส่ง Secret กลับ Browser
ตอนรัน Worker จึงค่อยโหลด Credential ที่ Active ถอดรหัส และ Inject Header ก่อน Dispatch ทันที Body, Status Text และ Response Header ที่อนุญาตจะผ่านการ Redact ด้วยค่าลับที่ระบบรู้จัก HTTP Client ยังบล็อก Destination ที่ไม่ปลอดภัย จำกัดเวลา จำนวน Redirect และขนาด Response พร้อมแปลง Transport Error เป็นรหัสที่ไม่เผยรายละเอียดภายใน ส่วน cURL Import ถูกมองเป็น Input ที่ไม่น่าเชื่อถือและมีหน้าที่สร้าง Preview เท่านั้น ไม่ใช่ทางลัดข้ามกฎ Security
Execution ที่จริงจังต้องเป็น State Machine ในฐานข้อมูล
เมื่อกด Run Browser ไม่ได้เป็นคนไล่ Node บน Canvas Backend จะ Compile Path ที่เลือก แล้วเรียก Supabase RPC เพื่อสร้าง Execution สถานะ Queued พร้อม Stage สถานะ Pending ตามลำดับใน Transaction เดียว Record เก็บ Trigger, Target ถ้ามี, Mode, Source, Source Key, Workflow Revision, ความสัมพันธ์กับ Retry และ Initial Input ที่ถูกจำกัดขนาด Source Key มี Unique Constraint จึงใช้กันคำขอ Schedule หรือ Client ซ้ำได้
Go Runner อยู่ใน Process ของ Backend คอยตื่นเมื่อมีงานและ Poll คิว การ Claim ใช้ FOR UPDATE SKIP LOCKED ร่วมกับ Lease ที่หมดอายุได้ ทำให้ Worker หลายตัวไม่หยิบ Execution เดียวกัน ก่อน Dispatch ระบบโหลด Workflow ใหม่แล้วเทียบ updatedAt กับ Revision ที่บันทึกตอน Queue หาก Workflow ถูกแก้หลังจากนั้น งานจะจบด้วย workflow_changed แทนการรัน Graph คนละเวอร์ชันใต้ Execution เดิม
แต่ละ Stage จะบันทึก Input ที่ Sanitize แล้ว เปลี่ยนเป็น Running เรียก Node จากนั้นเก็บ Output หรือ Error และต่ออายุ Lease การ Cancel เป็นแบบ Cooperative งานที่ยัง Queued ยกเลิกได้ทันที ส่วนงานที่ Running จะเปลี่ยนเป็น Cancellation Requested และถูกตรวจระหว่าง Stage รวมถึงก่อน/หลัง Dispatch การ Retry สร้าง Execution ใหม่ที่ Link กลับตัวเดิม จึงไม่เขียนทับหลักฐานเก่า
การ Recovery มี Trade-off ที่ตั้งใจให้ระมัดระวัง หาก Lease หมดตอน External Request กำลังทำงาน Worker ตัวใหม่ไม่อาจรู้ได้ว่า Side Effect ฝั่งปลายทางเกิดแล้วหรือยัง ระบบจึงหยุดด้วย dispatch_state_unknown แทนยิงซ้ำโดยเดา นี่คือการยอมเสีย Automatic Recovery บางส่วนเพื่อป้องกันผลข้างเคียงซ้ำ
Supabase เป็นคิวของ Studio ส่วน Redis ไม่ใช่
Backend ติดต่อ Supabase ผ่าน PostgREST และ RPC ไม่ได้เปิด Direct SQL Connection สำหรับ Application Path ใน Studio Postgres เป็นทั้ง Durable Record และจุดประสานงาน Definition, Queue Row, Stage Transition, Lease, Idempotency, Cancellation และ Audit จึงอยู่ในขอบเขตเดียวกัน Migration แบบ Additive ค่อย ๆ เพิ่ม Definition JSON, Encrypted Credential, Graph Queue, Stage I/O, Ownership Check และความปลอดภัยของ Schedule/Webhook
Redis มีอยู่จริงใน Backend แต่ซอร์สไม่ได้ใช้มันเป็น Execution Queue ของ Studio RedisCache ถูกผูกกับ Article Cache และ Fixed-window Rate Limit แบบแชร์หลาย API Instance พร้อมเส้นทาง Fallback เมื่อ Redis ไม่พร้อม การให้ Postgres เป็น Queue ช่วยเลี่ยงปัญหาประวัติอยู่ที่หนึ่งแต่งานค้างอยู่อีกที่ แลกกับ Polling และจำนวน Database Write ที่มากขึ้น การเขียน Architecture ที่ตรงควรบอกขอบเขตนี้ ไม่ใช่วาด Redis ไว้กลางระบบเพียงเพราะ Dependency มีอยู่
Observability ต้องย้อนกลับไปหาหลักฐานได้
Timeline ของ Execution ไม่ได้เป็น Animation ที่ Frontend สร้างเอง StudioExecutionStage เก็บ Node ID/Type, Position, Status, เวลา, Input/Output ที่จำกัดขนาด, Error ที่ผ่านการทำความสะอาด และ Detail หน้า Admin ที่มีสิทธิ์จึงตรวจข้อมูลระดับ Stage ได้ ขณะที่ Public Stage และ SSE Projection จะล้าง Input, Output, Metadata และ Error ส่วนตัวออกก่อน Serialize อย่างชัดเจน
Browser ต่อ SSE ผ่าน Proxy แบบ Same-origin ของ Next.js Backend Poll สถานะที่ Persist อยู่ Hash Snapshot แล้วส่งเฉพาะตอนข้อมูลเปลี่ยน มี Heartbeat และจำกัดอายุ Stream ก่อนสั่ง Client Reconnect ฝั่ง React ตรวจ Shape ของ Snapshot และแสดงสถานะ Connecting, Live, Reconnecting หรือ Fallback พร้อม Backoff แบบมีเพดาน ดังนั้น SSE ช่วยให้สดขึ้นแต่ไม่ใช่ Source of Truth ต่อให้ Refresh หน้า Timeline ก็สร้างกลับจาก Supabase ได้
Audit Log เป็นมุมมองอีกชุดหนึ่ง การแก้ Workflow, Execution, Node และ Credential บันทึก Actor กับบริบทของ State ส่วน Operational Log เลือกรายงาน Identifier, Hostname และชนิด Error แทน Request Secret Deployment ใช้ Readiness Endpoint ที่ Fail Closed และ Probe ตาราง Workflow/Execution จริง แยกจาก Health Endpoint ที่บอกเพียงว่า Process ยังตอบสนอง
ส่ง Frontend และ Backend แยกกันโดยมีจุดย้อนกลับ
ทั้งสองแอปใช้ Multi-stage Container และ Runtime User ที่ไม่ใช่ Root ฝั่ง Studio ติดตั้งและ Build ด้วย Bun เป็น Next.js Standalone Artifact ก่อนนำไปรันด้วย Node ส่วน Backend Compile Go Binary แบบ Static ใน Builder ที่ใช้ Cache แล้ววางใน Alpine Image ขนาดเล็กซึ่งมี CA Certificate และ Timezone Data
GitHub Actions บังคับ Validation ก่อน Publish Studio รัน Bun Test, Biome, Production Build และตรวจ Syntax ของ Deploy Script ฝั่ง Backend ตรวจ gofmt, go vet, Test ที่เปิด Race Detector และ Shuffle, golangci-lint, ตรวจ Script, Build Image จริง และ Smoke Test Image เดียวกับที่จะส่งขึ้น GHCR Image Tag เป็น Full Commit SHA ไม่ใช่ Tag ที่เปลี่ยนความหมายได้
การ Deploy ใช้ SSH Host Identity ที่ Pin ไว้ แล้วอัปเดต Compose Service บน VPS แบบ Serialize Backend จะผ่านได้ต่อเมื่อ Readiness Probe ที่ตรวจ Persistence สำเร็จ หากไม่สำเร็จ Script คืน Image ก่อนหน้า Manual Rollback ก็เลือก Full SHA ที่ Publish แล้ว ไม่ Build Source เก่าใหม่อีกครั้ง ข้อดีของการแยก Release คือเปลี่ยนแต่ละส่วนได้อิสระ แต่ลำดับต้องชัด: Migration แบบ Additive ก่อน ตามด้วย Backend ที่ Compatible แล้วจึงเปิด UI Capability
ทางลัดที่เคยดูดี แต่ไม่ผ่านเกณฑ์ Production
หลายแนวคิดทำ Demo ได้เร็วแต่สร้างระบบที่อธิบายไม่ได้ รายชื่อ Label วาด Node ได้แต่รักษา Execution Semantics ไม่ได้ การปล่อยให้วาด Edge อิสระขัดกับ Worker แบบเส้นเดียว การเรียก Trigger Test ว่าเป็น Workflow Run ทำให้ Output ชั่วคราวปะปนกับ Lifecycle จริง การคืน Stage JSON แบบ Public เสี่ยงเปิดเผยข้อมูลงาน การฝัง Credential ใน Definition ทำให้ Secret กระจายไปทุกพื้นผิว และการใช้ Overview Endpoint ที่มี Fallback เป็น Deploy Gate อาจรายงานผ่านแม้ Persistence ล่ม
ข้อจำกัดบางอย่างยังคงอยู่และถูกเปิดเผยตรงไปตรงมา Palette มี Node มากกว่าที่ Runtime รันได้ Compiler รองรับ Shared Linear Chain ยังไม่ใช่ Graph ที่มี Branch และ Join อิสระ Schedule Loop อยู่ใน API Process และ Scan Workflow จากฐานข้อมูล แม้มี Database Idempotency คุม Occurrence แต่ยังไม่ใช่ Distributed Scheduler โดยเฉพาะ SSE อาศัย Polling ไม่ใช่ Database Push และการแก้ Credential กับการเขียน Audit เป็น Operation ที่อยู่ติดกัน หาก Audit ล้ม API อาจรายงานปัญหาหลัง Credential เปลี่ยนไปแล้ว
การบอกข้อจำกัดเหล่านี้ไม่ได้ทำให้งานดูเล็กลง ตรงกันข้าม มันทำให้ผู้อ่านแยกได้ว่าอะไร Shipped อะไรตั้งใจ Fail Closed และอะไรควรเป็น Phase ถัดไป
สิ่งที่เปลี่ยนวิธีคิดหลังสร้างระบบนี้
บทเรียนหลักคือออกแบบย้อนจากหลักฐานที่ต้องการเห็น หากหน้า Execution ต้องบอก Input, Output, เวลา และสาเหตุที่ล้ม ข้อมูลเหล่านั้นต้องเป็น Field ที่ Persist ก่อนเริ่มแต่ง Timeline หาก Node จะใช้ Auth การป้องกัน Secret ต้องครอบคลุม Draft, Storage, Dispatch, Error และ Public Projection ก่อนเปิด Control หากสอง Trigger ใช้ปลายทางร่วมกัน Compiler ต้องพิสูจน์ Path ไม่ใช่ให้ Screenshot เป็นหลักฐานแทน
อีกบทเรียนคือข้อจำกัดทำให้ Visual System น่าเชื่อถือขึ้นได้ Derived Edge, Root ที่เลือกชัดเจน, Path เดียวที่ Execute ได้, Revision Check, Payload ที่มีขอบเขต และ Unsupported Node ที่ Fail Closed ยืดหยุ่นน้อยกว่า Automation Platform ทั่วไป แต่ทุกสถานะที่แสดงอธิบายได้ง่ายกว่า
AI Workflow Studio จึงไม่ได้ถูกวางตัวเป็นคู่แข่งของ n8n แต่เป็น Case Study ของระบบที่เชื่อม Visual Contract เข้ากับ Durable State Machine โดยให้ Security และ Observability มากพอที่แม้การรันล้มเหลว เหตุการณ์นั้นก็ยังเหลือเป็นหลักฐานสำหรับตรวจสอบและตัดสินใจต่อได้