اپنے اے آئی ایجنٹ کو ایک اعصابی نظام دیں: 90 منٹ کا فوری کورس
15 تصورات، حقیقی استعمال کا تقریباً 80%: حواس (triggers)، اضطراری ردِعمل (durable execution)، اور توازن (flow control)۔
آپ نے ایک ایسا ایجنٹ بنا لیا ہے جو کام کرتا ہے۔ مسئلہ یہ ہے: یہ صرف اُسی وقت کام کرتا ہے جب آپ اسے دیکھ رہے ہوں۔ آپ Claude Code یا OpenCode کھولتے ہیں، ٹائپ کرتے ہیں، یہ جواب دیتا ہے۔ ذرا ہٹ جائیں اور یہ رک جاتا ہے۔ اس خلا کو پاٹنا، یعنی اُس ایجنٹ کے درمیان جسے آپ چلاتے ہیں اور اُس Worker کے درمیان جو خود اپنے طور پر چلتا ہے، یہی اس کورس کا موضوع ہے۔
اس خلا کو جو چیز پاٹتی ہے، وہ کوئی زیادہ ذہین ایجنٹ نہیں۔ آپ کے ایجنٹ کے پاس کام کرنے کے لیے درکار سب کچھ پہلے سے موجود ہے: سوچنے کے لیے ایک LLM، عمل کے لیے tools اور MCP servers، اور ان کاموں کے لیے skills جو وہ جانتا ہے۔ جو اس میں کم ہے، وہ ایک اعصابی نظام ہے۔
اپنے جسم کے بارے میں سوچیں۔ آپ کا دماغ سوچتا ہے اور آپ کے پٹھے عمل کرتے ہیں۔ مگر نیچے ایک دوسرا نظام چل رہا ہوتا ہے، آپ کے بغیر: آپ کے دل کی دھڑکن، آپ کے اضطراری ردِعمل، وہ سگنل جو آپ کے سونے کے دوران آپ کو زندہ رکھتے ہیں۔ توجہ ہٹا لیں، تب بھی آپ کا دل دھڑکتا رہتا ہے۔ ایجنٹ کے پاس اس جیسا کچھ نہیں۔ تو جس لمحے آپ اسے چلانا بند کرتے ہیں، یہ رک جاتا ہے۔
ایک اعصابی نظام بغیر کسی انسان کے ہر باری میں شامل ہوئے، خود ہی اس loop کو مکمل کرتا ہے۔ یہ دنیا کو محسوس کرتا ہے اور جب کچھ ہوتا ہے تو ایجنٹ کو جگاتا ہے۔ جب کوئی step ناکام ہوتا ہے تو یہ reflex سے ردِعمل دیتا ہے، اور کسی شخص یا سست API کا انتظار کرتے ہوئے گھنٹوں اپنی جگہ تھامے رکھتا ہے۔ جب ایک ساتھ پانچ سو درخواستیں آتی ہیں تو یہ ایجنٹ کو مستحکم رکھتا ہے۔ یہی فرق ہے اُس ایجنٹ میں جسے آپ چلاتے ہیں اور اُس FTE میں جو خود اپنے طور پر چلتا ہے۔ آپ یہ اعصابی نظام اپنے ایجنٹ میں جوڑتے ہیں۔ آپ ایجنٹ کو دوبارہ نہیں لکھتے۔ یہی وہ ایک خیال ہے جس پر یہ پورا کورس بنا ہے۔
📚 تدریسی معاون
مکمل پریزنٹیشن دیکھیں — اے آئی ایجنٹ کا اعصابی نظام
اس ٹول کا ایک تکنیکی نام ہے: durable execution engine۔ ہم اس میں سے ایک استعمال کرتے ہیں جسے Inngest کہتے ہیں۔ یہی patterns Temporal، Restate، اور Dapr Agents میں بھی کام کرتے ہیں۔ اور یہ صرف ایک تدریسی تصویر نہیں ہے: Day AI، جو اے آئی-native کمپنیوں کے لیے ایک CRM ہے، Inngest کو اپنی پروڈکٹ کا "اعصابی نظام" کہتی ہے۔ Inngest کی مفت Hobby tier شروع کرنے کے لیے سب سے آسان جگہ ہے: کوئی credit card نہیں، ایک کمانڈ والا dev server، اور ایک dashboard جسے آپ build کرتے ہوئے دیکھ سکتے ہیں۔
کسی بھی چیز سے پہلے، پورا setup ایک تصویر میں یہ ہے:
1. an EVENT happens (e.g. a customer emails)
|
v
2. the INNGEST ENGINE catches it
(you do NOT build this. it runs your agent for you:
retries, waits, remembers every step, shows a dashboard)
|
| it reaches your code over a thin web wire (FastAPI)
v
3. YOUR AGENT runs
(the only part you write. it thinks and acts.)
یہی پورا ماڈل ہے: دو پروگرام۔ engine (جسے آپ نہیں لکھتے) events پکڑتا ہے اور آپ کا agent (جسے آپ لکھتے ہیں) چلاتا ہے، ایک پتلے web wire کے ذریعے اس تک پہنچتے ہوئے، اور یہی واحد وجہ ہے کہ اس کورس میں کبھی کوئی web server (FastAPI) نظر آتا ہے۔ آپ Quick Win میں دونوں شروع کرتے ہیں اور دیکھتے ہیں کہ engine آپ کے agent کو کیسے چلاتا ہے۔
مثال جان بوجھ کر چھوٹی ہے: ایک کسٹمر سپورٹ ایجنٹ جو چند نمونہ کسٹمرز کو دیکھتا ہے، ایک جواب کا مسودہ بناتا ہے، اور refund صرف کسی انسان کے منظور کرنے کے بعد جاری کرتا ہے۔ ایجنٹ مشکل حصہ نہیں ہے، اس لیے ہم اسے چھوٹا رکھتے ہیں اور اپنی محنت اس کے گرد موجود اعصابی نظام پر لگاتے ہیں۔ آپ اسے یہاں شروع سے بناتے ہیں۔ یہ پہلے والے Digital FTE کورس کے ختم ہونے کی جگہ سے آگے بڑھاتا ہے، اگرچہ اگر آپ نے وہ کورس چھوڑ دیا ہو تو D0 شروع سے ایک کم سے کم Worker تیار کر دیتا ہے۔ یہ Python-first ہے، inngest-py پر: آپ اپنے عام ایجنٹ کو سادہ انگریزی میں ہدایت دیتے ہیں، اور وہ code لکھتا ہے۔
یہ کورس کیسے بنا ہے، تاکہ آپ اسے درست طریقے سے پڑھیں، یہ رہا۔ build ہی ریڑھ کی ہڈی ہے۔ آپ environment ایک بار Quick Win میں سیٹ کرتے ہیں، پھر Part 4 سات مختصر prompts میں پورا Worker بناتا ہے، ایک وقت میں اعصابی نظام کی ایک پرت۔ یہی راستہ ہے، اور اسے عملاً کرنا ہی وہ طریقہ ہے جس سے ماڈل ذہن میں بیٹھتا ہے۔ Parts 1-3 میں موجود پندرہ تصورات وہ حوالہ ہیں جس پر build انحصار کرتا ہے: ہر ایک میں ایک خیال، اُس پرت کے نیچے کی "وجہ" جو آپ ابھی شامل کرنے والے ہیں۔ اس میں سے گزرنے کے دو اچھے طریقے ہیں۔ اگر آپ keyboard سے پہلے خیال پسند کرتے ہیں تو پہلے Parts 1-3 پڑھیں۔ یا سیدھے Quick Win اور Part 4 پر جائیں، اور جس لمحے کوئی پرت آپ سے پوچھے "مگر یہ اس طرح کیوں کام کرتا ہے؟" اُس تصور میں واپس جھانک لیں۔ ہر صورت، Part 4 ہی وہ جگہ ہے جہاں آپ build کرتے ہیں۔
ایجنٹ کبھی اعصابی نظام کو import نہیں کرتا، اس لیے آپ Inngest کو Temporal یا Restate سے بدل سکتے ہیں اور ایجنٹ کو بے چھوئے چھوڑ سکتے ہیں۔ کسی کام کے بیچ میں ایک اکیلے ایجنٹ کا crash ہو جانا پریشان کن ہے۔ پچاس ایجنٹس کی ایک افرادی قوت جو کسٹمر کے سامنے کا کام نیچے اعصابی نظام کے بغیر سنبھالے، ناممکن ہے: یا تو آپ ایسا پلیٹ فارم اپناتے ہیں جو یہ آپ کو دے دے، یا چھ مہینے خود اس کا ایک بدتر نسخہ بنانے میں لگاتے ہیں۔ چار خصوصیات اس اعصابی نظام کو ایجنٹس کے لیے منفرد طور پر اہم بناتی ہیں: Day AI، جو اے آئی-native کمپنیوں کے لیے CRM ہے، اپنی پروڈکٹ ہر اُس primitive پر چلاتی ہے جو یہ کورس سکھاتا ہے: durable LLM workflows، wait-for-event coordination، ناکامی پر replay، debounce اور throttle اور concurrency، اور multi-tenant fairness۔ اُن کے دو بانی انجینئرز نے آزادانہ طور پر خود ہی اسی اعصابی نظام والی تصویر کی طرف ہاتھ بڑھایا۔ یہ production کی زبان ہے، curriculum کی برانڈنگ نہیں۔ Agent Factory کا مقالہ سات مستقل اصول (Invariants) بیان کرتا ہے جو کسی بھی production ایجنٹ سسٹم کو پورے کرنے ضروری ہیں۔ جو Worker آپ یہاں بناتے ہیں وہ Invariant 4 (ایک engine) اور Invariant 5 (system of record، یہاں ایک چھوٹا audit trail) پورا کرتا ہے۔ یہ کورس مزید دو شامل کرتا ہے، اور Invariant 1 کا ایک حصہ:ایک اے آئی ایجنٹ کو اعصابی نظام کیوں چاہیے (چار خصوصیات)
step.wait_for_event (تصور 15) کے بغیر آپ خود ایک approval queue بناتے ہیں: database table، polling، timeout handling، audit trail۔ یہ ایک پروجیکٹ ہے، کوئی feature نہیں۔یہ کورس Agent Factory کے مقالے میں کہاں بیٹھتا ہے
step.wait_for_event کسی بھی پلیٹ فارم پر سب سے صاف اظہار ہے: ایجنٹ معطل ہو جاتا ہے، ایک انسان متوقع event خارج کرتا ہے، ایجنٹ پھر چل پڑتا ہے۔
15 تصورات، ایک نظر میں۔ یہ اُن تین کاموں پر نقشہ بناتے ہیں جو ایک اعصابی نظام کرتا ہے: حواس (triggers Worker کو جگاتے ہیں)، اضطراری ردِعمل (durable execution اسے درست رکھتا ہے جب کچھ ٹوٹتا ہے)، اور توازن (flow control اسے بوجھ کے نیچے صحت مند رکھتا ہے)۔ یہ پہلے دور کا نسخہ ہے، تصور کے ساتھ ایک سطر کا خلاصہ۔ جب build کے دوران کچھ ٹوٹتا ہے، تو آخر میں موجود Quick reference میں ایک symptom-سے-concept تشخیصی فہرست ہے جو آپ کو واپس اُس تصور کی طرف بھیجتی ہے جس سے ناکامی تعلق رکھتی ہے۔15 تصورات، ہر ایک ایک سطر میں (مکمل نقشے کے لیے کھولیں)
# تصور ایک سطر کا خلاصہ حواس (Triggers) دنیا Worker تک کیسے پہنچتی ہے 1 Events بمقابلہ requests request sync ہوتی ہے اور کوئی انتظار کرتا ہے؛ event async ہوتی ہے اور دنیا آگے بڑھ چکی ہوتی ہے۔ 2 Cron triggers ایک schedule function کو جگاتا ہے۔ ایک سطر: TriggerCron(cron="0 9 * * *")۔3 Webhook triggers ایک inbound HTTP payload ایک نام والی event بن جاتی ہے؛ آپ کا function اُس نام پر ردِعمل دیتا ہے۔ 4 Idempotency اور event semantics Event IDs اور step names ایک نقل شدہ event (یا retry) کو no-op بنا دیتے ہیں۔ 5 Fan-out اور sub-agent delegation ایک event، N subscribing functions؛ یا ایک parent N child events داغتا ہوا۔ اضطراری ردِعمل (Durable execution) Worker کو درست رکھنا جب کچھ ٹوٹتا ہے 6 step.run اور durable function modelہر step.run ایک checkpoint ہے؛ function steps کے درمیان crash ہو کر resume کر سکتا ہے۔7 Memoization، نیچے کی مشینری مکمل شدہ steps دوبارہ چلنے کے بجائے محفوظ شدہ output واپس کرتے ہیں۔ 8 step.sleep اور step.wait_for_eventدونوں function کو durable طور پر معطل کرتے ہیں، کسی مدت کے لیے یا کسی event کے لیے۔ 9 Retries، error handling، dead-letter خودکار backoff retries؛ N کوششوں کے بعد ناکام run replay کے لیے باقی رہتا ہے۔ 10 Python میں AI calls کے لیے step.runOpenAI calls کو step.run میں لپیٹیں؛ step.ai.infer inference آف لوڈ کرتا ہے (step.ai.wrap صرف TypeScript کے لیے ہے)۔توازن اور بحالی بوجھ کے نیچے flow control، بحالی، اور انسانی gate 11 Concurrency اور throttling concurrency فعال runs کی حد باندھتا ہے؛ throttle per-second شروعات کی حد باندھتا ہے۔12 Priority اور fairness Priority queue کو ترتیب دیتا ہے؛ per-key concurrency ہر tenant کو منصفانہ حصہ دیتا ہے۔ 13 Batching سستے bulk کام کے لیے events کو ایک batched function call میں جمع کریں۔ 14 Replay اور bulk cancellation ناکام runs کو نئے code کے ساتھ replay کریں؛ جو runs آپ کو مزید نہیں چاہییں انہیں bulk-cancel کریں۔ 15 step.wait_for_event کے ساتھ HITL gatesfunction اُس وقت تک معطل رہتا ہے جب تک ایک انسان منظوری نہ دے، پھر فیصلے کے ساتھ resume ہوتا ہے۔
پیشگی شرائط۔ یہ کورس فرض کرتا ہے کہ آپ نے From Agent to Digital FTE کر رکھا ہے۔ اگر کیا ہے، تو آپ نیچے دی گئی ہر چیز پہلے سے پوری کرتے ہیں اور آپ کے پاس لپیٹنے کے لائق ایک Worker موجود ہے: Part 4 کا اعصابی نظام سیدھا اُسی کی طرف اشارہ کرتا ہے، اور آپ D0 میں شروع-سے-والا setup چھوڑ دیتے ہیں۔ اگر نہیں کیا، تو پہلے وہ کورس کر لیں، یا پھر بھی پڑھتے رہیں: D0 شروع سے ایک کم سے کم Worker بناتا ہے تاکہ باقی کورس خود اپنے پاؤں پر کھڑا رہے۔ ہر صورت، آپ کو چار چیزیں چاہییں۔
- آپ ایک عام ایجنٹ چلا سکتے ہیں۔ Claude Code یا OpenCode، install اور authenticate شدہ۔ Plan mode، rules files، read-first-then-write workflow: اگر یہ تال آپ کو مانوس ہے، تو آپ تیار ہیں۔ اگر نہیں، تو Agentic Coding Crash Course اسے سکھاتا ہے۔
- ایک
OPENAI_API_KEY(یا کوئی اور model key جسے آپ کا عام ایجنٹ استعمال کر سکے) اور Worker کے Postgres system of record کے لیے ایک Neon account۔ Worker ایک حقیقی model چلاتا ہے اور Neon میں اپنے customers اور audit trail کو پڑھتا اور لکھتا ہے۔ Neon مفت ہے (کوئی کارڈ نہیں)، اور آپ setup کے دوران ایک browser click سے اسے authorize کرتے ہیں؛ اگر آپ کے پاس account نہیں ہے تو neon.com پر تقریباً ایک منٹ میں sign up کر لیں۔ Inngest dev server کو خود کسی account کی ضرورت نہیں۔- Node.js 20+ دستیاب، حالانکہ Worker Python ہے۔ Inngest dev server ایک Node CLI کے طور پر تقسیم ہوتا ہے (
npx inngest-cli@latest dev)۔- "event-driven" بمقابلہ "request/response" کا ایک کام چلاؤ ذہنی ماڈل۔ اگر "دنیا ایک event داغتی ہے اور صفر، ایک، یا کئی functions اس پر ردِعمل دیتے ہیں" آپ کو مانوس لگتا ہے، تو آپ تیار ہیں۔ اگر نہیں، تو تصور 1 آپ کو اس کی شکل دیتا ہے۔
اس صفحے کو پہلے دور میں کیسے پڑھیں
دو دور۔ پہلا دور اعصابی نظام کا ماڈل، اس کی تین پرتیں، آپ کے ذہن میں ڈالتا ہے؛ دوسرا دور، Part 4 میں keyboard پر ہاتھ رکھے، وہ جگہ ہے جہاں آپ build کرتے ہیں۔ اگر آپ پہلے build کرنا اور ماڈل کو ساتھ ساتھ بنتا دیکھنا چاہیں، تو وہ بھی کام کرتا ہے: Quick Win سے شروع کریں، Part 4 چلائیں، اور ہر تصور کو وہ حوالہ سمجھیں جسے آپ تب کھولتے ہیں جب کوئی پرت "کیوں" اٹھائے۔ "Done when" یا "What to watch" کا لیبل لگی ہر چیز کو کھولیں: چلنے والا رویہ جس کے against آپ اپنی پیش گوئیاں جانچ سکیں۔ Part 4 میں آپ پہلی بار پڑھتے ہوئے load-bearing snippets پر سرسری نظر ڈال سکتے ہیں؛ ہر ایک کے گرد موجود نثر آپ کو بتاتی ہے کہ پرت کیا کرتی ہے، اور جب آپ build کرتے ہیں تو آپ کا ایجنٹ code لکھتا ہے۔ "Try with AI" والے بلاکس اختیاری توسیعی prompts ہیں۔ ہر تصور ایک Predict (پڑھنے سے پہلے ایک جواب پر قائم ہوں) یا ایک Quick check (ابھی پڑھا ہوا اصول جانچیں) کے ساتھ ختم ہوتا ہے؛ دونوں آپ کو رکنے پر مجبور کرنے کے لیے ہیں، آپ کو نمبر دینے کے لیے نہیں۔ ہر اصطلاح وہیں سیاق میں بیان کی گئی ہے جہاں وہ پہلی بار آتی ہے۔
مئی 2026 تک درست۔ پورا Part 4 build ایک live Inngest dev server اور ایک حقیقی model کے against سرے سے سرے تک چلایا گیا تھا، inngest 0.5.18، openai-agents 0.17.x (0.17.3 اور 0.17.4 دونوں پر build اور دوبارہ verify کیا گیا)، fastapi 0.136.3، Python 3.12، اور Inngest CLI پر۔ Part 4 کا ہر snippet اُسی چلتے ہوئے build سے ہے، یادداشت سے نہیں لکھا گیا۔ جو architecture یہ کورس سکھاتا ہے وہ SDK کے بدلنے سے نہیں بدلتی؛ SDK اس سال کا اس تک کا interface ہے۔ ایک جگہ جہاں openai-agents کا معمولی bump کاٹ سکتا ہے وہ D5 کی resume تفصیل ہے (run-state serialization ایک custom context کو کیسے سنبھالتا ہے)، اس لیے وہ Decision براہِ راست live docs سے جوڑتا ہے۔ اگر کوئی live docs صفحہ اور یہ صفحہ کبھی کسی syntax تفصیل پر اختلاف کریں، تو docs جیتتے ہیں: اپنے versions pin کریں، اور build کرتے وقت Inngest Python quick start اور OpenAI Agents SDK docs دیکھ لیں۔
وہ سیکشن جو Claude Code اور OpenCode کے درمیان مختلف ہوتے ہیں ان میں ایک switcher ہوتا ہے؛ ایک چنیں اور صفحہ ہر دورے پر اسی کے مطابق ہو جاتا ہے۔
پندرہ منٹ کا quick win: base سیٹ کریں، اور reflex دیکھیں
اُن 15 تصورات سے پہلے جو سمجھاتے ہیں کہ یہ کیوں کام کرتا ہے، وہ environment سیٹ کریں جس میں یہ کورس چلتا ہے اور دیکھیں کہ ایک کام کیسے crash سے بچ نکلتا ہے۔ آپ یہ setup ایک بار کرتے ہیں؛ Part 4 اسی base پر اصل Worker بناتا ہے۔ آخر تک آپ کے پاس ہوگا:
- آپ کے عام ایجنٹ میں کھلا base، جس کی skills اور tools آپ کے لیے سیٹ کر دیے گئے ہوں،
- ایک تازہ Neon database جس میں دو tables (
customersاورaudit_log) ہوں جنہیں آپ کے ایجنٹ نے بنایا، - ایک چھوٹا سا Worker چلتا ہوا، ایک dashboard کے ساتھ جہاں آپ اسے دیکھ سکتے ہیں،
- ایک ایسا run جسے آپ نے انتظار کے دوران سوتے ہوئے دیکھا، اور پورے وقت zero compute خرچ ہوا،
- ایک ایسا run جسے آپ نے جان بوجھ کر توڑا، پھر سسٹم کو retry کرتے دیکھا: اس نے وہ کام رکھا جو پہلے ہی مکمل ہو چکا تھا اور صرف وہ حصہ دوبارہ چلایا جو ٹوٹا تھا،
- اور وہی function جس میں ایک حقیقی ایجنٹ durable step کے اندر greeting لکھ رہا ہے، تاکہ آپ ختم پر ایک اے آئی Worker کو چلتے ہوئے دیکھ چکے ہوں، صرف ایک timer نہیں۔
آخری دو نکتے ہی اصل بات ہیں: retry وہ reflex ہے جس کے بارے میں یہ پورا کورس ہے، اور اس کے اندر چلتا ہوا ایجنٹ وہ وعدہ ہے جو reflex پورا کرتا ہے۔ یہ ایک ہی نشست ہے، پورا Part 4 build نہیں، تو اسے کریں، پھر تصورات کے لیے واپس آئیں۔
اب آپ شروع والے دو پروگرام چلاتے ہیں: آپ کا worker (آپ کا code) اور Inngest dev server (اس کے ساتھ چلنے والا engine، جس کا dashboard http://127.0.0.1:8288 پر ہے، جہاں /runs ہر run کی فہرست دیتا ہے)۔ یہ ایک چھوٹے، ہمیشہ-آن web layer، FastAPI، کے ذریعے جڑتے ہیں، وہ دروازہ جس پر dev server دستک دے کر run شروع کرتا ہے۔ پورا loop ایک سطر میں: ایک event آتی ہے، dev server اُس دروازے کے ذریعے آپ کے worker تک پہنچتا ہے، آپ کا durable function ایک وقت میں ایک step چلاتا ہے، اور ہر step dashboard میں ریکارڈ ہوتا ہے۔ آپ کا عام ایجنٹ آپ کے لیے دونوں لکھتا اور شروع کرتا ہے؛ آپ کا کام دیکھنا ہے۔
ایک اور حد اہم ہے، وہی جو Digital FTE کورس نے کھینچی تھی۔ آپ کا worker اپنے customers اور اپنا یہ ریکارڈ کہ اس نے کیا کیا، ایک Neon database میں رکھتا ہے، اور اُس database کو دو مختلف طریقوں سے چھوا جاتا ہے۔ جب آپ build کرتے ہیں، آپ کا عام ایجنٹ سادہ انگریزی میں آپ کے لیے Neon میں پہنچتا ہے، tables بنانے اور rows چیک کرنے کے لیے۔ جب worker چلتا ہے، تو یہ اسی database سے اپنے ایک عام connection کے ذریعے بات کرتا ہے۔ build-time والا tool کبھی چلتے ہوئے worker میں جوڑا نہیں جاتا؛ Neon کے اپنے docs صاف کہتے ہیں کہ یہ build اور inspect کرنے کے لیے ہے، production کے لیے نہیں۔ Neon ایک click پر مفت ہے؛ Inngest dev server کو سرے سے کسی account کی ضرورت نہیں۔
base حاصل کریں اور اسے کھولیں
base ڈاؤن لوڈ کریں اور folder کو اپنے عام ایجنٹ میں کھولیں۔ ایجنٹ خود setup کرتا ہے، نیچے دیے گئے prompts سے۔ آپ یہ setup ایک بار کرتے ہیں: ai-agent-nervous-system/ پورے کورس کے لیے آپ کا folder ہے، Quick Win اور Part 4 دونوں کے لیے۔ آپ کبھی دوبارہ ڈاؤن لوڈ یا دوبارہ unzip نہیں کرتے۔
ai-agent-nervous-system-base.zip ڈاؤن لوڈ کریں
cd ai-agent-nervous-system
claude
cd ai-agent-nervous-system
opencode
یہ base ایک باصلاحیت عام ایجنٹ فرض کرتا ہے (Claude Code، یا OpenCode جو Claude Sonnet یا Opus، GPT-5، یا ملتا جلتا چلا رہا ہو)۔ ایک چھوٹا model build prompt پر بھٹک جائے گا؛ اگر اس کا پہلا plan مخصوص ہونے کے بجائے مبہم لگے، تو آگے بڑھنے سے پہلے کسی مضبوط پر سوئچ کریں۔
base کو تیار کریں (تقریباً 3 منٹ)
base اپنے rules AGENTS.md میں اور اپنی MCP wiring بھیجتا ہے؛ Skills، آپ کی key، اور Neon authorization اس کے بعد آتے ہیں۔ اپنے ایجنٹ سے خود کو سیٹ کروائیں۔ یہ paste کریں:
Read AGENTS.md, then get this base ready: install the Skills it lists for whichever agent you are, copy
.env.exampleto.envfor me, and tell me exactly what you need from me to bring the Neon and Context7 MCP servers online.
اس پر نظر رکھیں: ایجنٹ چار Inngest Skills اور neon-postgres Skill install کر رہا ہے (آپ install runs اور Installed تصدیقات دیکھتے ہیں)، .env بنا رہا ہے، پھر آپ سے دو چیزیں مانگ رہا ہے: .env میں paste کرنے کے لیے آپ کی OPENAI_API_KEY، اور OAuth پر Neon کو authorize کرنے کے لیے ایک browser click۔ Neon مفت ہے؛ اگر آپ کے پاس ابھی account نہیں ہے تو neon.com پر تقریباً ایک منٹ میں sign up کر لیں، یا authorization screen پر ہی ایک بنا لیں۔ INNGEST_DEV=1 پہلے سے .env میں ہے، اس لیے SDK بغیر signing key کے local dev mode میں چلتا ہے۔ جب install اور wiring مکمل ہو جائے، ایجنٹ آپ کو dev server شروع کرنے (اگلا step) اور پھر اسے restart کرنے کا کہتا ہے، کیونکہ نئی Skills اور inngest-dev MCP درمیانِ session لوڈ نہیں ہوتے۔
Done when: Skills install ہو چکی ہوں، .env میں آپ کی key ہو، Context7 قابلِ رسائی ہو، اور Neon authorize ہو۔ inngest-dev MCP اُس وقت آن لائن آتا ہے جب dev server چل رہا ہو، جو اگلا step ہے۔
dev server شروع کریں، اور تصدیق کریں کہ ایجنٹ اس تک پہنچ سکتا ہے (تقریباً 2 منٹ)
یہ کورس دو حدیں شامل کرتا ہے جن تک آپ کا ایجنٹ MCP پر پہنچتا ہے: ایک Neon database جسے یہ build اور inspect کرتا ہے، اور چلتا ہوا dev server جسے یہ events بھیجتا اور دیکھتا ہے۔ تو کچھ بھی build کرنے سے پہلے، دونوں کو لائیں اور تصدیق کریں کہ یہ live ہیں۔
Inngest dev server کو اپنے الگ terminal میں شروع کریں (یہ ایک Node CLI ہے؛ اسے چلتا چھوڑ دیں):
npx inngest-cli@latest dev
dashboard http://127.0.0.1:8288 پر آتا ہے، اور dev server اپنا MCP endpoint /mcp پر فراہم کرتا ہے۔ اب اپنے عام ایجنٹ کو restart کریں (ai-agent-nervous-system folder میں exit کر کے دوبارہ launch کریں) تاکہ تازہ install شدہ Skills اور inngest-dev MCP دونوں لوڈ ہو جائیں۔ پھر یہ paste کریں:
List the Neon tools and the inngest-dev tools you can see.
اس پر نظر رکھیں: دو حقیقی فہرستیں۔ Neon tools (ایک project بنانا، SQL چلانا، tables بیان کرنا، connection string لانا، وغیرہ) database پر آپ کے ایجنٹ کا ہاتھ ہیں۔ inngest-dev tools (list_functions، send_event، invoke_function، get_run_status، اور باقی) چلتے ہوئے dev server پر اس کا ہاتھ ہیں۔ نیچے کی ہر چیز دونوں پر سوار ہے۔
Gate open: جواب حقیقی Neon tool names اور حقیقی inngest-dev tool names کی فہرست دیتا ہے۔ اگر Neon tools غائب ہیں: OAuth مکمل نہیں ہوا؛ prep step سے Neon authorization دوبارہ کریں۔ اگر inngest-dev tools غائب ہیں: dev server چل نہیں رہا (اسے شروع کریں)، یا آپ نے restart چھوڑ دیا (exit کریں، اس folder میں دوبارہ launch کریں، دوبارہ پوچھیں)۔
store بنائیں، اور اس کی connection string پکڑیں (تقریباً 3 منٹ)
اب Neon MCP پر worker کا system of record بنائیں، پھر worker کو وہ ایک چیز تھمائیں جو اسے بعد میں اس تک پہنچنے کے لیے درکار ہوگی: ایک connection string۔ جو Worker آپ Part 4 میں بناتے ہیں وہ اپنے customers یہاں پڑھتا ہے اور اپنا audit trail یہاں لکھتا ہے۔ یہ paste کریں:
Paste this to your general agent. Plan first; execute on approval.
On a fresh Neon project, create two tables:
customers(id, email, tier) andaudit_log(a record of every action the worker takes). Then call the Neon tool that returns the connection string and write that URL into my.envasDATABASE_URL. Use the Neon tools for all of it; don't write SQL for me to run.
اس پر نظر رکھیں: ایجنٹ Neon MCP tools کو call کر رہا ہے تاکہ project اور دونوں tables بنائے (آپ وہ tool calls دیکھتے ہیں، آپ کے ٹائپ کردہ SQL نہیں)، پھر .env میں DATABASE_URL لکھ رہا ہے۔ وہ string ہی handoff ہے: Neon MCP نے store فراہم کیا، اور آپ کا worker وہ string استعمال کرے گا، MCP server نہیں۔
Done when: ایک تازہ Neon project موجود ہو جس میں ایک customers table اور ایک audit_log table ہو، اور .env میں ایک DATABASE_URL ہو۔ console.neon.tech کھولیں، وہ project چنیں جو ایجنٹ نے ابھی بنایا، اور Tables کھولیں: وہاں customers اور audit_log موجود ہیں، ابھی خالی۔ آپ D0 میں worker کے چلنے پر rows ظاہر ہوتے دیکھیں گے۔ (ایک table بس ایک spreadsheet ہے: ہر row ایک چیز، ہر column ایک تفصیل۔)
پہلا durable function بنائیں، اور اسے اپنے ایجنٹ سے چلائیں (تقریباً 3 منٹ)
اب سب سے چھوٹا durable function بنائیں، اُن Skills کا استعمال کرتے ہوئے جو آپ نے ابھی install کیں۔ Inngest Skills اپنی مثالوں میں TypeScript-first ہیں، اس لیے آپ کا ایجنٹ اُن سے patterns لیتا ہے (step کیا ہوتا ہے، durable function کی شکل کیسی ہوتی ہے) اور صحیح Python signatures docs سے تصدیق کرتا ہے (dev-server MCP کا grep_docs/read_doc، یا Context7)، یادداشت سے نہیں۔ یہ paste کریں:
Using the Inngest Skills, write one tiny Inngest durable function (call it
greet-customer, triggered by ademo/greetevent) that composes a greeting in onestep.run, sleeps fifteen seconds withstep.sleep, then composes a farewell in a secondstep.runand returns both. Serve it from a FastAPI host in local dev mode, and start the host on port 8000 with auto-reload on, so edits I make later are picked up without a manual restart.
جو شکل یہ لکھتا ہے، تاکہ آپ اسے دیکھ کر پہچان لیں: function سادہ async def ہے، دونوں step.run calls وہ کام لپیٹتی ہیں جسے memoize ہونا چاہیے، اور ان کے درمیان step.sleep run کو durable طور پر معطل کرتا ہے۔ اُس sleep کے دوران process crash، restart، یا redeploy ہو سکتا ہے، اور timer داغنے پر run پھر بھی اگلی line سے resume ہوتا ہے۔ ایجنٹ کے code میں تصدیق کرنے کی ایک تفصیل: Inngest client is_production=False کے ساتھ بنایا گیا ہو، یا یہ آپ کے .env میں پہلے سے موجود INNGEST_DEV=1 پڑھتا ہو۔ ان میں سے کسی ایک کے بغیر، SDK خاموشی سے Cloud پر default ہو جاتا ہے اور آپ کا function کبھی local register نہیں ہوتا۔
Done when: FastAPI host (پہلے والا دروازہ) port 8000 پر چل رہا ہو، اور dev server (پچھلے step سے پہلے ہی چل رہا ہے) نے اسے خودکار طور پر discover کر لیا ہو۔ ایجنٹ سے inngest-dev list_functions tool سے تصدیق کرنے کو کہیں (یا http://127.0.0.1:8288 کھولیں، Functions پر کلک کریں، اور greet-customer فہرست میں دیکھیں)۔ یہاں سے آپ اپنے ایجنٹ سے events بھیجتے ہیں اور runs کو dashboard میں دیکھتے ہیں۔
اسے trigger کریں، اور ایک step کو zero compute پر سوتا دیکھیں (آپ چلاتے ہیں)
trigger event اپنے ایجنٹ سے بھیجیں۔ یہ paste کریں:
Send a
demo/greetevent withnameSara using the inngest-devsend_eventtool.
(dashboard ترجیح دیتے ہیں؟ http://127.0.0.1:8288 میں Events پر کلک کریں، پھر Send event، نیچے دیا payload paste کریں، اور Send پر کلک کریں۔ کسی بھی طرح وہی run شروع ہوتا ہے۔)
{
"name": "demo/greet",
"data": { "name": "Sara" }
}
اب durable sleep دیکھیں، اور آپ کے پاس اسے live پکڑنے کے لیے تقریباً پندرہ سیکنڈ ہیں۔ دو طریقے، ایک چنیں:
- ایجنٹ کو poll کرنے دیں (ایجنٹ-native طریقہ): "Poll
get_run_statuson that run until it finishes." درمیانِ sleep ایجنٹ run کو Running بتاتا ہے، ابھی کوئی end time نہیں، اور آپ کا host terminal پورے وقت بیکار؛ پھر یہ Completed پر پلٹ جاتا ہے، output dict کے ساتھ اور تقریباً پندرہ سیکنڈ کا start-to-end فرق۔ وہی فرق sleep ہے۔ - dashboard دیکھیں:
http://127.0.0.1:8288→ Runs → سب سے نیا run، فوراً کھولیں۔ پہلا step مکمل ہو چکا ہے اور sleep step ایک resume time کے ساتھ Sleeping دکھاتا ہے؛ پندرہ سیکنڈ بعد یہ خود ہی resume ہو کر Completed پر پلٹ جاتا ہے، Output panel میں واپس کیا گیا dict۔
کسی بھی طرح، اُن پندرہ سیکنڈ کے دوران آپ کے code میں کچھ نہیں چلتا: dev server resume time تھامے رکھتا ہے اور host بیکار بیٹھا رہتا ہے۔ یہی اصل بات ہے، ایک durable wait کی قیمت zero compute ہے۔ (run کو ختم ہونے کے بعد کھولیں اور آپ کو بس Completed اور output نظر آئے گا، live sleep پہلے ہی جا چکا؛ دوبارہ بھیجیں اور تیزی سے دیکھیں، یا ایجنٹ کو poll کرنے دیں۔)
ایک step توڑیں، اور retry کو وہ کام چھوڑتے دیکھیں جو پہلے ہی ہو چکا (اصل صلہ)
اب ایک step کو جان بوجھ کر ناکام کریں، تاکہ آپ memoization کو مکمل شدہ کام retry کے پار لے جاتے دیکھ سکیں۔ یہ اپنے ایجنٹ کو paste کریں:
Make the farewell step raise an error on purpose, so I can watch a run fail. Keep everything else the same.
وہی demo/greet event دوبارہ بھیجیں، پھر ناکام ہوتے run کا dashboard میں per-step trace پڑھیں (Runs → newest)۔ یہ رہا اصل صلہ، اور یہ اِسی ایک ناکام run میں ہے: greeting step ایک مکمل کوشش دکھاتا ہے، اور farewell step کئی Attempts دکھاتا ہے، ہر ایک backoff کے ساتھ retry شدہ (Inngest کئی کوششوں پر default ہے) اس سے پہلے کہ run Failed پر آ جائے۔ اُس attempt count کے مطلب کے ساتھ ٹھہریں: مکمل ہوا greeting step ایک بار ادا ہوتا ہے، ہر retry پر ایک بار نہیں۔ یہ durable execution ہے جسے آپ اپنی آنکھوں سے دیکھ سکتے ہیں۔ کیوں مکمل شدہ step دوبارہ چلنے کے بجائے فوراً واپس آتا ہے، یہ وہ مشینری ہے جس سے آپ تصور 7 میں ملیں گے؛ ابھی، بس اسے ہوتے دیکھیں۔
اسے چلانے پر دو چیزوں کی توقع رکھیں:
- per-step ثبوت dashboard میں ہے، ایجنٹ میں نہیں۔ آپ کا ایجنٹ event داغتا ہے اور run-level status بتا سکتا ہے، مگر dev-server MCP کا
get_run_statusrun summary کوsteps: nullکے ساتھ واپس کرتا ہے؛ یہ per-step attempts کو نہیں کھولتا۔ وہ attempt counts جو ہی memo کا ثبوت ہیں (greeting ایک پر، farewell چڑھتا ہوا) dashboard Runs view میں رہتے ہیں۔ Quick Win میں یہ واحد جگہ ہے جہاں آپ browser کی طرف ہاتھ بڑھاتے ہیں، ایجنٹ کی طرف نہیں۔ - Failed تک پہنچنے میں چند منٹ لگتے ہیں۔ default retries اور exponential backoff کے ساتھ، run کئی منٹ تک farewell step کو retry کرتا رہتا ہے (ایک حقیقی run کو تقریباً ساڑھے چار منٹ لگے) اس سے پہلے کہ یہ Failed پر پلٹے۔ آپ کو انتظار نہیں کرنا پڑتا: memo کا ثبوت پہلے retry سے ہی نظر آنے لگتا ہے، greeting ایک کوشش پر تھمی رہتی ہے جبکہ farewell مزید جمع کرتا ہے۔ چند کوششیں دیکھیں، پھر آگے بڑھیں۔
(یہ dev-server build کوئی الگ "memoized" badge بھی نہیں دکھاتا۔ memo ہی attempt count ہے: مکمل ہوا step ایک کوشش پر بیٹھا اور ٹوٹا step چڑھتا ہوا، یہاں "memo سے واپس آیا، دوبارہ نہیں چلا" بالکل اسی طرح نظر آتا ہے۔)
اب اسے ٹھیک کریں:
Now revert the farewell step to the working version.
host خود ہی auto-reload ہو جاتا ہے (یہی --reload نے آپ کو دیا؛ اگر آپ نے یہ چھوڑ دیا، تو host کو ہاتھ سے restart کریں)۔ ایک تازہ demo/greet event بھیجیں اور اب پورا function ٹھیک شدہ code پر صاف چل کر Completed ہو جاتا ہے۔ بحالی کے بارے میں ایک چیز لوگوں کو الجھاتی ہے۔ dashboard کا Rerun بٹن آپ کے موجودہ code کے ساتھ اوپر سے ایک بالکل نیا run شروع کرتا ہے، ہر step شروع سے دوبارہ چلتا ہے۔ یہ incident recovery کے لیے درست tool ہے: ایک خراب deploy نے runs کا ایک گروہ توڑ دیا، تو آپ ایک fix بھیجتے ہیں اور انہیں rerun کرتے ہیں۔ مگر یہ memo-محفوظ رکھنے والا resume نہیں ہے۔ memo-محفوظ resume وہ خودکار retry ہے جو آپ نے ابھی ناکام run کے اندر دیکھا، جہاں مکمل ہوا step اپنی جگہ ٹھہرا رہا۔
اسے ایک حقیقی AI worker بنائیں (Part 4 کا پل)
اب تک function صرف strings کے ساتھ کھیلتا ہے، اور یہ جان بوجھ کر تھا: جب راستے میں کچھ اور نہ ہو تو durability دیکھنا آسان ہوتا ہے۔ اب greeting کو ایک اصل ایجنٹ سے آنے دیں، تاکہ آپ وہی اعصابی نظام ایک حقیقی AI call کو لے کر چلتا دیکھیں۔ ایک prompt hardcoded greeting کو ایک چھوٹے ایجنٹ سے بدل دیتا ہے؛ sleep، durability، اور dashboard سب جوں کے توں رہتے ہیں۔ یہ paste کریں:
Replace the hardcoded greeting with a one-line call to a minimal hello-world agent built on the OpenAI Agents SDK (it just writes the greeting), still inside the same
step.run. Keep thestep.sleepand the farewell unchanged. Then fire ademo/greetevent and show me the run.
واحد چیز جو بدلی ہے وہ یہ ہے کہ greeting step کو کیا بھرتا ہے: ایک f-string کے بجائے، ایک model اسے لکھتا ہے۔ اور چونکہ وہ call اُسی step.run کے اندر بیٹھتی ہے جسے آپ پہلے ہی durable ثابت کر چکے ہیں، یہ مفت میں memoized اور crash-safe ہے، کسی نئی wiring کے بغیر۔ run کو اسی طرح دیکھیں جیسے پہلے دیکھا تھا (ایجنٹ سے poll کریں، یا اسے dashboard میں کھولیں): وہی تین-step trace اور وہی zero-compute sleep، سوائے اس کے کہ پہلے step کا output اب ایک ایجنٹ سے آیا۔ آپ کی OPENAI_API_KEY prep step سے پہلے ہی .env میں ہے، اس لیے سیٹ کرنے کو کچھ نیا نہیں۔
Done when: ایک demo/greet run مکمل ہوتا ہے اور output میں greeting ایک ایجنٹ سے آیا، کسی hardcoded string سے نہیں۔ جس چیز کو آپ دیکھ رہے ہیں اس کے ساتھ ٹھہریں، کیونکہ یہ پورا کورس ایک جملے میں ہے: ایک اے آئی ایجنٹ، ایک event سے جاگا، ایک اعصابی نظام کے اندر durable طور پر چلتا ہوا، ایک crash سے بچ نکلا۔ Part 4 اس hello-world ایجنٹ کو ایک حقیقی کسٹمر سپورٹ worker سے بدل دیتا ہے اور اسے پورے اعصابی نظام میں لپیٹ دیتا ہے (ایک حقیقی event trigger، ایک cron جو fan out کرتا ہے، flow control، ایک انسانی منظوری gate)، مگر ابھی آپ کی screen پر جو شکل ہے، وہی اصل شکل ہے۔
آپ نے ابھی پورا کورس environment سیٹ کیا اور اعصابی نظام کو اپنی آنکھوں سے کام کرتے دیکھا: Skills install ہو چکی ہیں، آپ کا Neon store .env میں DATABASE_URL کے ساتھ تیار ہے، dev-server MCP live ہے، اور آپ نے ایک durable function چلایا، ایک step کو compute خرچ کیے بغیر سوتے دیکھا، ایک step توڑا اور خودکار retry کو مکمل شدہ step memo سے واپس کرتے دیکھا جبکہ صرف ٹوٹا step دوبارہ چلا، پھر اُسی durable step کے اندر ایک حقیقی ایجنٹ کو greeting بناتے دیکھا۔ یہی وہ architecture ہے جس کے بارے میں یہ کورس ہے۔ باقی کورس اسے بڑے پیمانے پر لے جاتا ہے: حقیقی حواس (cron، webhook، fan-out)، مضبوط تر اضطراری ردِعمل (step.run کے اندر ایجنٹ کی invocation)، بوجھ کے نیچے حقیقی توازن، اور وہ انسانی منظوری gate جو "ایجنٹ شاید یہ بگاڑ دے" کو "ایجنٹ مسودہ بناتا ہے، ایک انسان منظور کرتا ہے، عمل جاری ہوتا ہے" میں بدل دیتا ہے۔
اگر کچھ کام نہ کرے، تو چار مسئلے اس کا تقریباً سارا احاطہ کرتے ہیں:
- dev server function host تک نہیں پہنچ سکتا: تصدیق کریں کہ host port 8000 پر چل رہا ہے۔
- client Cloud mode میں ہے: ایجنٹ نے
is_production=Falseچھوڑ دیا اور.envمیںINNGEST_DEV=1غائب ہے، اس لیے functions کبھی local register نہیں ہوتے۔ اس سے ایک سیٹ کرنے کو کہیں (ایک واضحis_productionvalue env var پر غالب آتی ہے)۔ - function dashboard سے غائب ہے: host دوبارہ لوڈ نہیں ہوا؛ اسے restart کریں۔
- ایک run بغیر کسی error اور بغیر progress کے رک جاتا ہے: ایک de-synced host خاموشی سے رک جاتا ہے؛ host اور dev server دونوں کو ایک ساتھ restart کریں، اور ایک host کو ایک dev server کے against چلائیں۔ (ایک باریک سبب: اگر
:8288لیا ہوا تھا اور dev server8289+پر آیا، توinngest-devMCP URL کو دوبارہ point کرنا کافی نہیں؛ host پھر بھی:8288سے بات کرتا ہے۔ host پرINNGEST_BASE_URL=http://127.0.0.1:<port>سیٹ کریں تاکہ یہ نئے port پر dev server کے پیچھے چلے۔)
اگر آپ ان میں سے کسی سے ٹکرائیں، تو ہر جگہ کام آنے والی بحالی کی چال یہاں بھی کام کرتی ہے: "Something didn't work. Read the error, tell me in plain language what you see, and propose one fix I can approve."
آپ نے کیا بنایا، اور یہ کہاں بڑھتا ہے
environment سیٹ ہے: base کھلا ہے، Skills install ہو چکی ہیں، تینوں MCP servers wire ہیں (Neon، Context7، inngest-dev)، آپ کے Neon store میں customers اور audit_log tables ہیں جن میں .env کے اندر DATABASE_URL ہے، اور dev server چل رہا ہے۔ آپ نے وہ ایک خیال بھی اپنی آنکھوں سے دیکھا جس پر پورا کورس ٹکا ہے، durable execution کا reflex، اور اس کے اندر ایک حقیقی ایجنٹ کو چلتے دیکھا۔ Part 4 اُس hello-world ایجنٹ کو کسٹمر سپورٹ worker سے بدل دیتا ہے، اِسی base پر، اِسی folder میں: یہ اُن customers کو پڑھتا ہے اور اُن audit rows کو لکھتا ہے، پھر پوری چیز کو پورے اعصابی نظام میں لپیٹ دیتا ہے، ایک حقیقی event trigger، ایک روزانہ cron جو fan out کرتا ہے، flow control، اور refunds پر durable انسانی منظوری gate۔ Part 4 اس step.run-اور-step.sleep ڈھانچے کو ایک ایسے worker میں بڑھاتا ہے جو آپ کے Neon store پر حقیقی کام کرتا ہے۔ اگر یہ Quick Win کام کر گیا، تو آگے آنے والے تصورات سمجھاتے ہیں کہ ہر ٹکڑا اس شکل میں کیوں ہے۔
Part 1: حواس، دنیا Worker تک کیسے پہنچتی ہے
یہاں سے، Parts 1-3 build کے پیچھے کی حوالہ شیلف ہیں: پندرہ تصورات، ہر ایک میں ایک خیال، اُن تین کاموں کے مطابق گروپ شدہ جو ایک اعصابی نظام کرتا ہے۔ آپ انہیں سیدھا پڑھ سکتے ہیں، یا کسی ایک میں اُس وقت جھانک سکتے ہیں جب Part 4 کی کوئی پرت آپ سے پوچھے کہ یہ کیوں کام کرتا ہے۔ یہ پہلا گروہ حواس ہے۔
ایک اے آئی ایجنٹ جسے آپ ہاتھ سے call کرتے ہیں، اُسی وقت چلتا ہے جب آپ اسے call کریں۔ ایک حقیقی اے آئی Worker کے حواس ہوتے ہیں: یہ تب چلتا ہے جب دنیا اس تک پہنچے۔ ایک کسٹمر ای میل کرتا ہے، ایک webhook آتا ہے، ایک cron روزانہ 09:00 پر داغتا ہے، ایک اور worker کام سونپتا ہے۔ ان میں سے ہر ایک ایک آتا ہوا سگنل ہے، اور ایک trigger وہ طریقہ ہے جس سے ایجنٹ اسے محسوس کرتا ہے۔ Part 1 کے پانچ تصورات وہی حواس ہیں: event-driven ذہنی ماڈل، تین طریقے جن سے دنیا اندر پہنچتی ہے (cron، webhook، event)، وہ semantics جو دوہرے processing کو روکتی ہیں، اور وہ fan-out patterns جو ایک سگنل کو کئی workers جگانے دیتے ہیں۔
تصور 1: Events بمقابلہ requests، durable ذہنی تبدیلی
اس کورس میں جو کچھ آگے آتا ہے وہ سب ایک ذہنی تبدیلی پر ٹکا ہے: requests سے events کی طرف۔
ایک request ایک synchronous گفتگو ہے۔ کوئی call کرتا ہے؛ آپ سنبھالتے ہیں؛ آپ واپس کرتے ہیں؛ وہ آگے بڑھتے ہیں۔ ایک connection کھلا رہتا ہے؛ ایک انسان یا سروس انتظار کر رہی ہوتی ہے۔ اگر آپ crash کریں، تو caller کو ایک error ملتا ہے۔ ایک ایجنٹ جس سے آپ prompt پر بات کرتے ہیں ایک request ہے: آپ نے ٹائپ کیا، اس نے واپس stream کیا، گفتگو آپ کے terminal session کی ملکیت تھی۔
ایک event ایک asynchronous پیغام ہے۔ دنیا میں کچھ ہوا (ایک کسٹمر نے sign up کیا، ایک ای میل آئی، ایک payment کلیئر ہوئی)، اور خبر دینے والا اُس حقیقت کا ایک نام والا ریکارڈ خارج کرتا ہے۔ صفر، ایک، یا کئی functions اس event پر آزادانہ ردِعمل دیتے ہیں۔ کوئی connection کھلا نہیں رہتا۔ خبر دینے والا نہیں جانتا کہ کون سن رہا ہے، نتائج کا انتظار نہیں کرتا، اور بلاک نہیں ہوتا۔ دنیا آگے بڑھ چکی ہے۔
# A request: I'm here, waiting, blocking
result = await agent.handle_customer_message(text=user_input)
print(result) # I unblock when the agent finishes
# An event: I fire-and-forget
await inngest_client.send(events=[
inngest.Event(
name="customer/email.received",
data={"customer_id": "c-4429", "body": email_body, "subject": subject},
),
])
# I return immediately. Somewhere else, one or more Inngest
# functions react to this event on their own schedule.
ایک request producer کو انتظار کرواتی ہے؛ ایک event اسے آزاد کرتی ہے، اور محفوظ شدہ event ایک crash سے بچ جاتی ہے۔
یہ تبدیلی چھوٹی لگتی ہے۔ ہے نہیں۔ ایک بار جب آپ events میں سوچنے لگتے ہیں، تو durability اور scale تقریباً مفت میں نکل آتے ہیں، کیونکہ:
- producer کو consumer سست نہیں کر سکتا (email-receiver ایجنٹ کے جواب کا مسودہ مکمل کرنے کا انتظار نہیں کرتا)۔
- consumer کام کھوئے بغیر crash اور restart کر سکتا ہے (event durable طور پر محفوظ ہے؛ Inngest اسے دوبارہ پہنچاتا ہے)۔
- producers کو بدلے بغیر نئے consumers شامل کیے جا سکتے ہیں (ایک دوسرا function، مثلاً ایک analytics counter،
customer/email.receivedکو subscribe کر سکتا ہے بغیر اس کے کہ email-receiver کو خبر ہو)۔ - Backpressure ایک flow-control policy بن جاتی ہے، code کی تبدیلی نہیں (Inngest concurrency کی حد باندھتا ہے؛ producer داغتا رہتا ہے؛ events queue میں لگتے ہیں)۔
Predict. آپ کا کسٹمر سپورٹ worker ایک ای میل کا جواب دینے میں 8 سیکنڈ لیتا ہے: ایجنٹ کی reasoning کے تین سیکنڈ، دو MCP tool calls کے چار سیکنڈ، database write کا ایک سیکنڈ۔ peak load پر آپ کو ایک منٹ میں 50 ای میلیں ملتی ہیں۔ اگر آپ request ماڈل استعمال کریں (email parser ایجنٹ کے ختم ہونے تک بلاک رہتا ہے)، تو اس سے آپ کے email parser کے کتنے متوازی HTTP connections مراد ہیں؟ اگر آپ event ماڈل استعمال کریں (email parser ایک event داغ کر فوراً واپس آ جاتا ہے)، تو کتنے؟ اعتماد 1-5۔
جواب: request ماڈل کو تقریباً 7 متوازی parsers چاہییں (50/منٹ × 8 سیکنڈ تقریباً 6.7 متوازی handlers ہیں، اور تھوڑی گنجائش)۔ event ماڈل کو ایک parser چاہیے۔ یہ event داغتا ہے اور تقریباً 10ms میں واپس آ جاتا ہے، event queue 50/منٹ کا spike جذب کر لیتا ہے، اور Inngest functions queue کو اُس concurrency پر استعمال کرتے ہیں جس کی آپ اجازت دیں۔
یہی فرق اصل بات ہے۔ event "دنیا میں کیا ہوا" اور "worker اس بارے میں کیا کرتا ہے" کے درمیان ایک durable حد بن جاتی ہے، اور ہر اچھی چیز اُسی ایک قدم سے نکلتی ہے: producer کبھی انتظار نہیں کرتا، ایک crash شدہ consumer محفوظ شدہ event سے retry کرتا ہے، اور نئے consumers producer کو چھوئے بغیر جڑ جاتے ہیں۔ Events وہ طریقہ ہیں جس سے آپ کام کی timing کی ملکیت چھوڑ دیتے ہیں۔Try with AI
Walk me through three scenarios. For each, classify it as REQUEST-MODEL
or EVENT-MODEL, and explain which one fits better:
A) A user clicks "Submit refund request" in the support portal and
expects to see "Refund issued: $30" within 2 seconds.
B) A nightly cron job at 02:00 runs a customer-health-check across
all 5,000 customers and writes a report to Slack.
C) A customer sends an email to support@; we want a draft response
ready within 60 seconds for the on-call agent to review and send.
For each, name (a) what the human's expectation of timing is and
(b) what failure looks like if the model crashes mid-execution.
تصور 2: Cron triggers، وہ کام جو اس لیے چلتا ہے کہ وقت گزر گیا
سب سے سادہ trigger گھڑی ہے۔ ایک اے آئی Worker جو بہت سے کام کرتا ہے وہ بیرونی events کے ردِعمل نہیں ہوتے؛ وہ شیڈول شدہ کام ہوتے ہیں: روزانہ health reports، ہفتہ وار صفائیاں، گھنٹہ وار دوبارہ حساب۔ Inngest کا cron trigger code کی ایک سطر ہے۔
import inngest
@inngest_client.create_function(
fn_id="daily-customer-health-check",
trigger=inngest.TriggerCron(cron="0 9 * * *"), # 09:00 every day, UTC
)
async def daily_health_check(ctx: inngest.Context) -> dict[str, int]:
"""Run a customer-health pass for every Pro/Enterprise customer."""
customers = await ctx.step.run("fetch-pro-customers", fetch_pro_customer_ids)
# fan out: one event per customer, one worker run per event
events = [
inngest.Event(name="customer/health_check.requested", data={"customer_id": cid})
for cid in customers
]
await ctx.step.send_event("fan-out", events)
return {"customers_scheduled": len(customers)}
تین چیزیں دیکھنے لائق:
-
schedule بس معیاری cron syntax ہے۔
0 9 * * *ہر دن 09:00 UTC ہے؛*/15 * * * *ہر 15 منٹ ہے؛0 9 * * 1پیر کو 09:00 ہے۔ Inngest cron کو default طور پر UTC میں جانچتا ہے؛ اگر آپ کو کوئی الگ timezone چاہیے، تو آپ cron string کے آگے ہی prefix لگاتے ہیں (مثلاًTZ=Europe/Paris 0 12 * * 5)، کوئی الگ argument نہیں دیتے۔ -
function پھر بھی وہی durable steps استعمال کرتا ہے۔ cron-triggered ہو یا event-triggered، function کی شکل ایک جیسی ہے: side effects کے لیے
ctx.step.run، fan out کے لیےctx.step.send_event۔ Durability ویسے ہی کام کرتی ہے۔ Flow control ویسے ہی کام کرتا ہے۔ trigger بس یہ ہے کہ function کیسے شروع ہوتا ہے۔ -
cron output ایک عام Inngest function run ہے۔ یہ dashboard میں نظر آتا ہے، ایک run ID رکھتا ہے، ایک trace رکھتا ہے، replay کو سپورٹ کرتا ہے۔ اگر آپ کا پیر کی صبح والا cron run step 3 پر ناکام ہو جائے، تو منگل کا cron معمول کے مطابق چلے گا اور پیر کی ناکامی bug ٹھیک کرنے کے بعد replay کے لیے دستیاب رہے گی۔
اگر cron داغتے وقت آپ کی سروس down ہو تو کیا ہوتا ہے؟ یہی سوال ایک durable scheduler کو ایک نازک سے الگ کرتا ہے۔ Inngest کے cron runs اُسی لمحے durable طور پر ریکارڈ ہو جاتے ہیں جب schedule داغتا ہے۔ اگر آپ کا function endpoint ناقابلِ رسائی ہو، تو Inngest backoff کے ساتھ retry کرتا ہے جب تک یہ کامیاب نہ ہو یا retry کی حد نہ پہنچ جائے۔ 09:00 پر داغا گیا cron اس لیے "miss" نہیں ہوتا کہ 09:00 پر آپ کا deploy جاری تھا؛ run انتظار کرتا ہے، آپ deploy مکمل کرتے ہیں، run مکمل ہو جاتا ہے۔ development میں cron triggers کی ایک باریک بات جاننے لائق ہے: local dev server صرف اُس وقت crons داغتا ہے جب یہ چل رہا ہو۔ Production انہیں Inngest کے infrastructure پر چلاتا ہے، جو ہمیشہ چلتا رہتا ہے۔
Quick check. تین دعوے۔ ہر ایک کو True یا False نشان زد کریں۔ (a) اگر ایک cron function چلنے میں 45 منٹ لیتا ہے اور ہر 15 منٹ شیڈول ہے، تو کسی بھی وقت تین متوازی instances چل رہے ہوں گے۔ (b) آپ ایک cron-triggered function کے اندر
step.sleepاستعمال کر کے کام دن بھر میں پھیلا سکتے ہیں۔ (c) ایک cron-triggered function کو dashboard سے testing کے لیے ہاتھ سے بھی invoke کیا جا سکتا ہے۔
جوابات: (a) Concurrency policy پر منحصر ہے: default طور پر Inngest overlapping runs کو queue کر دے گا؛ اگر آپ concurrency=1 سیٹ کریں تو یہ سلسلہ وار چلتے ہیں؛ اگر آپ concurrency=10 سیٹ کریں تو یہ متوازی چلتے ہیں۔ default معقول ہے۔ (b) True، اور یہ "روزانہ کام کو گھنٹوں میں پھیلا کر load ہموار کرنے" کا ایک عام pattern ہے۔ (c) True: Inngest dashboard آپ کو کسی بھی function کو testing کے لیے طلب پر invoke کرنے دیتا ہے، اس کے trigger سے قطع نظر۔Try with AI
With my AI coding assistant connected to the Inngest dev server MCP,
write a cron-triggered Inngest function in Python that:
1. Runs every Monday at 09:00 UTC.
2. Queries the audit_log table for all conversations resolved in the
prior week (status='resolved' in that window).
3. Computes per-agent metrics: total conversations resolved, average
resolution time, count of escalations, count of refunds issued.
4. Returns the metrics as a JSON object.
After you write the function, test it now instead of waiting for
Monday: trigger it on demand from the Inngest dev dashboard (the
Invoke button), since the dev server only fires crons while it is
running. Confirm the audit query is correct by running the SQL
directly against the database and checking the rows it returns;
grep_docs can confirm your step.run pattern matches Inngest's
examples, but only running the query proves the SQL itself.
تصور 3: Webhook triggers، جب باہر کی دنیا اندر call کرتی ہے
پہلا trigger گھڑی تھی۔ دوسرا HTTP ہے: آپ کے سسٹم سے باہر کوئی چیز (Stripe، آپ کا email provider، آپ کی سائٹ پر ایک form، ایک GitHub event) آپ کے worker تک پہنچنا چاہتی ہے۔
اس بارے میں واضح رہیں کہ کون سا حصہ مشکل ہے، کیونکہ یہ وہ حصہ نہیں جس کا آپ اندازہ لگائیں گے۔ POST وصول کرنا آسان ہے: FastAPI جیسا web framework آپ کو تین سطروں میں @app.post(...) دیتا ہے۔ مشکل حصہ وہ سب کچھ ہے جو POST اترنے کے بعد ہے: call کو queue کرنا، ناکامی پر retry کرنا، درمیانِ کام crash سے بچنا، کسی redelivery کو دوہرا process کرنے سے انکار کرنا، ایجنٹ چلانا، چار گھنٹے کی منظوری تھامنا، کسی dashboard سے کوئی بھی run replay کرنا۔ دروازہ سستا ہے؛ اس کے پیچھے کا باورچی خانہ ہی اصل کام ہے، اور وہ باورچی خانہ Inngest ہے۔
تو route چھوٹا ہی رہتا ہے۔ اس کا پورا کام POST وصول کرنا، event کو Inngest کو سونپنا، اور تیزی سے 200 جواب دینا ہے۔ durable کام اس کے پیچھے Inngest function میں چلتا ہے۔ اگر آپ اس کے بجائے وہ کام request handler کے اندر کریں، تو آپ کلاسک webhook bugs سے ٹکرائیں گے: sender timeout ہو جاتا ہے اور آپ کے ابھی کام کرتے ہوئے دوبارہ بھیج دیتا ہے، ایک restart کام کھو دیتا ہے، ایک redelivery کسٹمر کو دو بار refund کر دیتا ہے۔ (Inngest کا hosted option ایک عوامی inn.gs/e/... URL بھی بنا سکتا ہے تاکہ آپ route لکھنا سرے سے چھوڑ دیں۔)
اب وہ حصہ جو سب کو الجھاتا ہے۔ آپ کی app کے پاس دو دروازے بن جاتے ہیں، اور وہ مخالف سمتوں میں ہوتے ہیں:
DOOR 1: the webhook door (you write it, or use the hosted URL)
Stripe knocks here with DATA -> it just calls send() and is done
DOOR 2: /api/inngest (auto-made by inngest.fast_api.serve)
the ENGINE knocks here to RUN YOUR CODE, one step at a time
it speaks Inngest's own protocol, so a raw Stripe POST here is rejected
یہ دونوں کبھی براہِ راست ایک دوسرے سے بات نہیں کرتے۔ وہ صرف event کے ذریعے جڑتے ہیں: Door 1 ایک event گراتا ہے، engine اسے اٹھاتا ہے اور آپ کا function چلانے کے لیے Door 2 سے واپس آتا ہے۔ Door 2 خودکار بنانا (جو Quick Win پہلے ہی کر چکا) Door 1 کے لیے کچھ نہیں کرتا؛ وہ وہی ہے جو آپ پھر بھی لکھتے ہیں۔
تو webhook دروازہ اصل میں کیا call کرتا ہے؟ بس send()۔ پورا route اتنا چھوٹا ہے:
@app.post("/webhooks/stripe")
async def stripe_webhook(request: fastapi.Request):
payload = await request.json()
# verify the signature, reshape Stripe's envelope, then hand it off:
await inngest_client.send(
inngest.Event(name="stripe/charge.refund.failed", data=reshape(payload)),
)
return {"ok": True} # ack Stripe in milliseconds
وہ send() event کو Inngest کی stream میں گرا دیتا ہے اور route ختم ہو جاتا ہے۔ یہ آپ کا function call نہیں کرتا، اور یہ /api/inngest کو call نہیں کرتا۔ Inngest وہ آدھا حصہ سنبھالتا ہے: یہ event name کو on_refund_failed سے ملاتا ہے اور function کے steps چلانے کے لیے Door 2 سے واپس آتا ہے۔ سرے سے سرے تک:
Stripe → Door 1 (webhook) → send() → Inngest → Door 2 (/api/inngest) → your function
@inngest_client.create_function(
fn_id="handle-stripe-refund-failed",
trigger=inngest.TriggerEvent(event="stripe/charge.refund.failed"),
)
async def on_refund_failed(ctx: inngest.Context) -> dict[str, str]:
"""Triggered by Stripe webhook → Inngest event → this function."""
charge_id = ctx.event.data["charge_id"]
# Find the support ticket this refund belongs to
ticket = await ctx.step.run(
"find-ticket-for-refund", lookup_ticket_by_charge, charge_id,
)
# Hand the support worker the full context.
# step.run takes (step_id, handler, *args): pass args positionally, not as kwargs.
await ctx.step.run(
"notify-support-agent",
notify_support_agent_of_refund_failure,
ticket["id"], charge_id,
)
return {"ticket": ticket["id"], "action": "notified"}
یہ دروازے کے پیچھے کا function ہے: Inngest نے event کو اس سے ملایا اور اسے چلایا، ticket ڈھونڈا اور support worker کو خبر دی، queue، retries، اور idempotency سب آپ کے لیے سنبھالے ہوئے۔ Webhook کام تقریباً ہمیشہ اسی طرح asynchronous ہوتا ہے: function تیز ack کے بعد background میں چلتا ہے، کبھی request کے دوران نہیں۔
دو patterns جو ایک نام کے قابل ہیں:
- Generic JSON webhooks. Sender کا کسی مشہور vendor ہونا ضروری نہیں۔ JSON POST کر سکنے والی کسی بھی سروس کو اُسی طرح کے URL کی طرف point کریں اور event name خود چنیں۔
vendor/event.subtypeانداز بس ایک convention ہے، مگر جب آپ اس کی پیروی کریں تو dashboard events کو صاف ستھرا گروپ کرتا ہے۔ - Webhook transforms. Vendor payloads بڑے اور nested ہوتے ہیں، اور ایک vendor اکثر ایک ہی URL پر کئی event types بھیجتا ہے۔ ایک transform ایک چھوٹا reshaping function ہے جو payload کے پہنچتے ہی Inngest کے servers پر چلتا ہے، اس سے پہلے کہ یہ ایک event بنے۔ (یہ JavaScript میں لکھا جاتا ہے چاہے آپ کا worker Python ہو، کیونکہ یہ Inngest کی طرف چلتا ہے، آپ کی app میں نہیں۔) یہ دو کام کرتا ہے: آپ کا event name چنتا ہے، اور payload کو اُن چند fields تک سکیڑتا ہے جو آپ واقعی استعمال کرتے ہیں۔ آپ کا function code vendor-specific JSON سے آزاد رہتا ہے۔
Predict. ایک Stripe webhook بالکل اُسی ملی سیکنڈ پر
stripe/charge.refund.failedداغتا ہے جب آپ کا کسٹمر سپورٹ worker بھیcustomer/refund.investigation_neededنام کا ایک مختلف event خارج کرنے کے لیےinngest_client.sendcall کر رہا ہے۔ دونوں events بیک وقت سسٹم میں آتے ہیں؛ اوپر والا function صرف Stripe event پر trigger ہوتا ہے۔ کیا function ایک بار چلے گا یا دو بار؟ اعتماد 1-5۔
جواب: ایک بار۔ ایک function صرف اُس event name کے لیے داغتا ہے جس کے ساتھ یہ register ہے۔ stripe/charge.refund.failed اور customer/refund.investigation_needed مختلف نام ہیں، اس لیے یہ مختلف functions (یا کسی کو نہیں) جگاتے ہیں، چاہے یہ ایک ہی لمحے میں اترے ہوں۔ event name ہی routing key ہے۔
یہی وجہ ہے کہ نام رکھنا کوئی سجاوٹ نہیں ہے۔ ایک ٹائپو، customer/email_received جہاں function customer/email.received کا منتظر ہو، اور function خاموشی سے کبھی نہیں چلتا۔ کسی چیز میں error نہیں آتا؛ کام بس نہیں ہوتا۔ dashboard آپ کا حفاظتی جال ہے: جو events کسی function سے نہیں ملتے وہ ایک الگ unmatched stream میں نظر آتے ہیں جسے آپ دیکھ سکتے ہیں۔
Locally، paste کرنے کو کوئی URL نہیں۔ اوپر کی ہر چیز production کا راستہ ہے۔ آپ کے laptop پر کوئی عوامی URL نہیں، اور Stripe localhost تک نہیں پہنچ سکتا۔ تو build کرتے ہوئے، آپ خود webhook کا کردار ادا کرتے ہیں: send_event (یا dev dashboard کا "Send to Dev Server" بٹن) بالکل وہی event داخل کرتا ہے جو ایک حقیقی webhook نے بنایا ہوتا۔ یہی وجہ ہے کہ نیچے کا عملی حصہ send_event سے test کرتا ہے اور Stripe کو کبھی نہیں چھوتا۔
یہ تقسیم تھامے رکھنے لائق ہے:
| event اندر کیسے آتی ہے | |
|---|---|
| Production | Stripe آپ کے live webhook URL پر POST کرتا ہے؛ یہ آپ کی stream میں ایک event بن جاتی ہے |
| Local dev (آپ) | آپ پہلے سے شکل دی ہوئی event کو send_event سے داخل کرتے ہیں |
آپ کا function code کسی بھی طرح ایک جیسا ہے؛ یہ صرف event name پر ردِعمل دیتا ہے اور کبھی نہیں جانتا کہ event ایک حقیقی webhook سے آئی یا آپ کے send_event سے۔Try with AI
I need to handle three webhook sources for my customer-support worker:
A) Stripe: refund failed, charge disputed
B) Postmark (email service): bounced email, complaint
C) My internal admin UI: manual "investigate this ticket" button
For each, decide:
1. What event names you'd use (vendor/event.subtype format).
2. Whether the function reacting to it should run synchronously (the
caller is waiting) or asynchronously (fire and continue).
3. Whether you'd write a webhook transform to reshape the payload, or
consume it raw.
Then write the Inngest function for the Stripe refund-failed case in
Python, using the MCP's grep_docs to find the current syntax for
TriggerEvent and the dev-server MCP's send_event tool to test it.
تصور 4: Idempotency، جب ایک ہی event دو بار آتی ہے
ایک ہی event کبھی کبھار آپ تک دو بار پہنچے گی۔ ایک کسٹمر "Issue refund" پر کلک کرتا ہے، صفحہ سست ہے، اور کلک دو بار داغتا ہے؛ یا request گزر جاتی ہے مگر caller کو واپس acknowledgment کھو جاتا ہے، اس لیے caller دوبارہ کوشش کرتا ہے۔ کسی بھی طرح آپ کا worker اب ایک حقیقی refund کے لیے دو customer/refund.requested events دیکھتا ہے۔ اگر یہ ہر ایک پر refund جاری کرے، تو کسٹمر کو دو بار refund ہو جاتا ہے۔
یہ event systems میں سب سے عام bug ہے، کوئی نادر edge case نہیں۔ Senders اُس وقت تک retry کرتے رہتے ہیں جب تک انہیں acknowledgment نہ ملے (networks packets گراتے ہیں، servers restart ہوتے ہیں، endpoints timeout ہوتے ہیں)، اس لیے جو آپ سے وعدہ کیا جاتا ہے وہ at least once delivery ہے، کبھی exactly once نہیں۔ علاج یہ ہے کہ دوسری نقل کو بے ضرر بنا دیا جائے: پہلی پر عمل کریں، نقل کو پہچانیں، اسے چھوڑ دیں۔ اس خاصیت کا ایک نام ہے۔ کوئی چیز idempotent ہوتی ہے جب اسے دو بار چلانے کا نتیجہ وہی رہے جو ایک بار چلانے کا تھا۔
Inngest اس کی دو پرتیں بنا کر دیتا ہے۔
پرت 1: Event ID ماخذ پر بیج ڈالتی ہے۔ جب آپ خود کوئی event بھیجتے ہیں (بجائے اسے کسی webhook سے وصول کرنے کے)، تو آپ ایک idempotency key منسلک کر سکتے ہیں:
await inngest_client.send(events=[
inngest.Event(
name="customer/refund.requested",
data={"order_id": "o-4429", "amount_cents": 5000},
id=f"refund-request-{order_id}", # idempotency key: identical on every retry
),
])
اگر اسی id کے ساتھ ایک دوسری event dedup window (default طور پر 24 گھنٹے) کے اندر بھیجی جائے، تو Inngest نقل کو گرا دیتا ہے۔ وہی منطقی event، وہی id، صرف ایک function run۔ key کا ہر نقل پر ایک جیسا ہونا لازم ہے، یہی پوری بات ہے۔ اسے request کی کسی مستحکم چیز سے بنائیں (یہاں، order id)، کبھی کسی timestamp یا random value سے نہیں، جو ہر بھیجنے پر بدلتی ہے اور خاموشی سے dedup کو شکست دیتی ہے۔
اسی طرح آپ اس سیکشن کے شروع والے retry شدہ webhook کو شروع سے قابو کرتے ہیں۔ آپ webhook event پر id براہِ راست سیٹ نہیں کرتے، مگر جو بھی POST کو ایک event میں بدلتا ہے (hosted transform، یا آپ کا اپنا وصول کرنے والا route) اسے provider کی اپنی event id سے سیٹ کرتا ہے۔ Stripe ہر event پر ایک منفرد id لگاتا ہے اور retry پر اسے بے تبدیل دوبارہ بھیجتا ہے، اس لیے دوبارہ پہنچایا گیا webhook وہی id لے کر آتا ہے اور بالکل ایک self-sent event کی طرح dedup ہو جاتا ہے۔
پرت 2: Step-level idempotency. ایک function کے اندر، ہر step.run کو اس کے نام سے پہچانا جاتا ہے۔ اگر کوئی function step 3 اور step 4 کے درمیان crash کرے، تو retry function code کو اوپر سے دوبارہ چلاتا ہے، مگر steps 1، 2، اور 3 کے لیے، Inngest step body کو دوبارہ چلائے بغیر محفوظ شدہ outputs واپس کرتا ہے۔ Step 4 پہلی بار معمول کے مطابق چلتا ہے۔ یہی وہ چیز ہے جو ایک function کو "durable" بناتی ہے: مکمل شدہ steps کے side effects retry پر دوبارہ نہیں ہوتے۔
@inngest_client.create_function(
fn_id="issue-customer-refund",
trigger=inngest.TriggerEvent(event="customer/refund.requested"),
)
async def issue_refund(ctx: inngest.Context) -> dict[str, str]:
# Step 1: look up the order. If the function retries, this returns
# the SAME order data it computed the first time, from Inngest's memo.
order = await ctx.step.run(
"lookup-order", lookup_order_by_id, ctx.event.data["order_id"],
)
# Step 2: call Stripe. If the function retries AFTER this step
# succeeded, the Stripe call does NOT happen again. The refund is
# issued exactly once even if the function runs three times.
refund = await ctx.step.run(
"issue-stripe-refund",
lambda: call_stripe_refund_api(
charge_id=order["stripe_charge_id"],
amount=ctx.event.data["amount_cents"],
),
)
# Step 3: write the audit row. Same property: runs at most once.
await ctx.step.run(
"audit-refund",
lambda: write_audit_refund_issued(order_id=order["id"], refund=refund),
)
return {"refund_id": refund["id"]}
اگر یہ function step 3 کے دوران crash کرے، تو retry step 1 میں دوبارہ داخل ہوتا ہے (cached order data لیتا ہے، کوئی DB call نہیں)، step 2 میں دوبارہ داخل ہوتا ہے (cached refund data لیتا ہے، کوئی Stripe call نہیں)، step 3 کو حقیقت میں چلاتا ہے، واپس آتا ہے۔ کسٹمر کا کارڈ ایک بار چارج ہوتا ہے، چاہے function تین بار چلا ہو۔ یہی وہ خاصیت ہے جو سب سے زیادہ اہم ہے۔ یہی Inngest کو retry loop والی queue سے معیاری طور پر مختلف بناتی ہے۔
(Step 1 اپنا ایک argument positionally پاس کرتا ہے۔ Steps 2 اور 3 اپنی call کو اس کے بجائے ایک lambda میں لپیٹتے ہیں، کیونکہ step.run صرف positional arguments آگے بھیجتا ہے، اس لیے ایک lambda وہ طریقہ ہے جس سے آپ کسی step کو ایسی call تھماتے ہیں جو keyword arguments استعمال کرتی ہے۔ دونوں شکلیں کام کرتی ہیں، اور lambda step body کو ایک خود مکتفی اکائی بھی بنا دیتا ہے جسے Inngest memoize کر سکتا ہے۔)
Memoization function کے نقطۂ نظر سے exactly-once step completion دیتا ہے: ایک بار جب ایک step کامیاب ریکارڈ ہو جائے، یہ کبھی دوبارہ نہیں چلتا۔ مگر ایک تنگ کھڑکی ہے۔ اگر کوئی step Stripe کو call کرے اور process Stripe کے چارج کرنے کے بعد مگر Inngest کے نتیجہ ریکارڈ کرنے سے پہلے مر جائے، تو retry Stripe کو دوبارہ call کرتا ہے، کیونکہ Inngest کے نزدیک step کبھی ختم نہیں ہوا۔ علاج یہ ہے کہ step memoization کو provider کی اپنی idempotency key (Stripe کا Idempotency-Key header، یا جو بھی dedup id آپ کے دوسرے providers فراہم کریں) کے ساتھ جوڑ دیا جائے۔ دونوں ایک دوسرے کے تکمیلی ہیں، متبادل نہیں: step.run آپ کے function کی internal logic کو exactly-once رکھتا ہے؛ provider کی key بیرونی side effect کو exactly-once رکھتی ہے۔
Quick check. True یا False۔ (a)
step.runstep کو صرف اُسی صورت idempotent بناتا ہے جب اندر کا function بھی idempotent ہو۔ (b) dedup window سے باہر کسی نقل شدہ ID والی event کو ایک نئی event سمجھا جائے گا۔ (c) اگرstep.runدرمیانِ execution ناکام ہو جائے (step کا code کوئی exception پھینکے)، تو Inngest ناکامی کو محفوظ کر لیتا ہے اور پہلے کے steps کو دوبارہ چلائے بغیر step کو اگلی کوشش پر retry کرتا ہے۔
جوابات: (a) False: step.run step کو اپنے طور پر at-most-once-on-success دیتا ہے؛ اسے اندر کے code کے idempotent ہونے کی ضرورت نہیں۔ ایک بار جب step کامیاب ریکارڈ ہو جائے، اس کا body retry پر کبھی دوبارہ نہیں چلتا۔ واحد استثنا اوپر کے note والی کھڑکی ہے: اگر process Stripe کے چارج کرنے کے بعد مگر Inngest کے step ریکارڈ کرنے سے پہلے مر جائے، تو retry Stripe کو دوبارہ call کرتا ہے، جو بالکل وہی وجہ ہے کہ provider idempotency key اس کی پشت پناہی کرتی ہے۔ آپ کے function کی internal logic، البتہ، آپ کو کبھی ہاتھ سے idempotent نہیں بنانی پڑتی۔ (b) True: Inngest کی dedup window default طور پر 24 گھنٹے ہے؛ اُس window کے بعد اسی ID والی events کو نئی سمجھا جاتا ہے۔ (c) True: خودکار retry memoized ہے؛ Inngest جانتا ہے کہ step 3 کوشش 1 پر ناکام ہوا اور صرف step 3 کو کوشش 2 پر retry کرتا ہے۔ پہلے کے کامیاب steps دوبارہ نہیں چلتے۔ (یہ within-run retry ہے، dashboard کا Replay بٹن نہیں، جو ایک تازہ run ہے، تصور 14۔)Try with AI
Here are three scenarios. For each, decide: idempotency PROBLEM or
NO PROBLEM, and if it's a problem, what's the fix:
A) Stripe sends the same charge.refund.failed webhook three times
in 90 seconds (because their first two attempts timed out at
your endpoint). Your function emails the customer.
B) A customer clicks "Issue refund" three times because the page
was slow. Your function calls Stripe and writes audit_log.
C) Your nightly cron at 09:00 sends a customer-health-check event
to each Pro customer. If two crons fire at the same time (a deploy
bug), what happens?
For each problem case, propose ONE specific fix: event ID seed
inside the function, idempotency key in inngest_client.send, or
function-level deduplication on the trigger.
تصور 5: Fan-out اور sub-agent delegation، ایک event کئی workers
اکثر ایک اکیلی event کو کئی جگہ کام trigger کرنا پڑتا ہے۔ Stripe کے charge.refund.failed event کو شاید یہ کرنا پڑے: support agent کو خبر دینا، audit لکھنا، کسٹمر کا risk score اپ ڈیٹ کرنا، finance ops کو الرٹ کرنا، Slack پر پوسٹ کرنا۔ پانچ ردِعمل، سب آزاد، سب ایک event سے۔
Inngest pattern: کئی functions کو اسی event سے subscribe کریں۔ کوئی fan-out code نہیں؛ بس اسی TriggerEvent کے ساتھ کئی @inngest_client.create_function decorators۔ ہر function آزادانہ چلتا ہے، اپنے retries رکھتا ہے، اپنا step trace رکھتا ہے، دوسروں سے آزاد ناکام ہوتا ہے۔
@inngest_client.create_function(
fn_id="refund-failed-notify-support",
trigger=inngest.TriggerEvent(event="stripe/charge.refund.failed"),
)
async def notify_support(ctx: inngest.Context) -> dict[str, str]:
# ... runs the customer-support worker to draft a response ...
return {"status": "drafted"}
@inngest_client.create_function(
fn_id="refund-failed-update-risk-score",
trigger=inngest.TriggerEvent(event="stripe/charge.refund.failed"),
)
async def update_risk_score(ctx: inngest.Context) -> dict[str, float]:
# ... runs the risk-scoring worker ...
return {"new_risk_score": 0.42}
@inngest_client.create_function(
fn_id="refund-failed-post-slack",
trigger=inngest.TriggerEvent(event="stripe/charge.refund.failed"),
)
async def post_to_slack(ctx: inngest.Context) -> None:
# ... posts a Slack notification ...
return None
ایک Stripe webhook آتا ہے۔ Inngest ایک event بناتا ہے۔ تین functions داغتے ہیں، ہر ایک اپنے run میں۔ اگر post_to_slack اس لیے ناکام ہو کہ Slack down ہے، تو باقی دونوں بے اثر رہتے اور معمول کے مطابق مکمل ہوتے ہیں۔ ناکام run Slack کے بحال ہونے پر replay کے لیے dashboard میں بیٹھا رہتا ہے۔ یہ multi-worker coordination کا مرکز ہے، اور یہ وہ architectural pattern ہے جسے آپ کی مستقبل کی manager پرت (ایک بعد کا کورس) بڑے پیمانے پر ترتیب دے گی۔
دوسرا fan-out pattern: parent-N-children-داغتا-ہے۔ کبھی fan-out متحرک ہوتا ہے۔ آپ کے روزانہ cron کو ہر Pro کسٹمر کے لیے ایک customer-health event داغنا ہوتا ہے، جو ہفتے کے مطابق 500 یا 5,000 ہو سکتے ہیں۔ parent function N events بھیجتا ہے:
async def fan_out_per_customer_events(
ctx: inngest.Context,
customers: list[str],
run_day: str, # pinned by the caller (the cron's scheduled date), never date.today()
) -> int:
events = [
inngest.Event(
name="customer/health_check.requested",
data={"customer_id": cid},
id=f"daily-health-{cid}-{run_day}", # stable id: identical on every retry
)
for cid in customers
]
# ctx.step.send_event memoizes the send, so a retry of this function
# does not re-fire the fan-out (and even if it did, the stable ids dedup).
await ctx.step.send_event("fan-out", events)
return len(events)
وہ 5,000 events ایک send_event step میں نکلتے ہیں (ایک بڑی list کو پسِ پردہ چند batched calls میں ٹکڑوں میں بانٹا جاتا ہے، لفظی طور پر ایک HTTP request نہیں)۔ 5,000 function runs داغتے ہیں، ہر ایک اپنے customer_id کے ساتھ، ہر ایک isolated، ہر ایک آزادانہ retry کے قابل۔ Flow control (تصور 11) حد باندھتا ہے کہ کتنے بیک وقت چلیں تاکہ آپ اپنے downstream APIs کو نہ پگھلائیں۔ cron function سیکنڈوں میں واپس آتا ہے؛ fan-out اُس rate پر چلتا ہے جس کی Inngest کی flow-control policies اجازت دیں۔
Sub-agent delegation fan-out کا ایک خاص معاملہ ہے۔ ایک worker run کے اندر، آپ مزید events بھیج کر sub-tasks کو دوسرے worker types کو سونپتے ہیں (await ctx.step.send_event(...)، تاکہ delegation کسی بھی دوسرے step کی طرح memoized ہو)۔ parent بچوں کا انتظار نہیں کرتا جب تک یہ صراحتاً step.invoke استعمال نہ کرے (جو ایک child function چلاتا ہے اور اس کے نتیجے کا انتظار کرتا ہے) تاکہ ان کے نتائج جمع کرے۔
Predict. آپ کے پاس تین functions ہیں جو سب
customer/email.receivedسے trigger ہوتے ہیں: کسٹمر سپورٹ ایجنٹ جو جواب کا مسودہ بناتا ہے (15 سیکنڈ)، ایک analytics counter (50ms)، اور ایک "VIP detector" جو چیک کرتا ہے کہ کسٹمر high-value ہے یا نہیں (200ms)۔ جب ایک ای میل آتی ہے، تو ہر ایک کے لیے user-visible latency کیسی لگتی ہے؟ تین اختیارات: (a) تینوں مل کر تقریباً 15 سیکنڈ بنتے ہیں؛ (b) تینوں متوازی چلتے ہیں، کل latency تقریباً 15 سیکنڈ ہے (سب سے سست)؛ (c) ہر ایک بغیر کسی مشترکہ latency کے آزادانہ چلتا ہے۔ اعتماد 1-5۔
جواب: (c)۔ ہر function اپنا run ہے، اپنے process slot میں۔ کسٹمر سپورٹ ایجنٹ analytics counter کو بلاک نہیں کرتا؛ VIP detector ایجنٹ کو بلاک نہیں کرتا۔ باہر سے، کسی خاص function کی latency بس اُسی function کا اپنا وقت ہے۔ یہی وجہ ہے کہ fan-out scale کرتا ہے: consumers isolated ہیں، اور اگر ایجنٹ crash کرے تو analytics counter بے اثر رہتا ہے۔ ایک شرط، جسے تصور 11 آگے بڑھاتا ہے: یہ isolation مختلف functions کے درمیان ہے۔ جب ایک اکیلا function اپنے ہی ہزاروں runs کی طرف fan out کرے، تو ایک concurrency cap جان بوجھ کر بعد کے runs کو queue کرواتا ہے، تو وہی same-function siblings اپنی باری کا انتظار کرتے ہیں۔ مختلف functions کبھی ایک دوسرے کو بلاک نہیں کرتے؛ ایک ہی function کے کئی runs کر سکتے ہیں۔Try with AI
Design the fan-out architecture for these three scenarios. For each,
sketch the event names and the functions that subscribe:
A) New customer signs up. Need to: send welcome email, create
Stripe customer, post to Slack #new-customers, write to
audit_log, schedule a 7-day follow-up.
B) Customer support email arrives. Need to: draft a reply (agent),
detect sentiment, check if VIP, update customer's "last contact"
timestamp, attach to the right ticket thread.
C) Daily cron at 09:00 needs to run customer-health-check on
~5,000 Pro customers. Each check takes ~30 seconds. We want
the whole batch to complete by 11:00 (a 2-hour window).
For each, decide: how many event types, how many subscriber
functions, what the idempotency story is, and one specific failure
mode this design protects against.
Part 2: اضطراری ردِعمل، جب کچھ ٹوٹتا ہے تو کیا ہوتا ہے
Part 1 اس بارے میں تھا کہ کام worker تک کیسے پہنچتا ہے۔ Part 2 اس بارے میں ہے کہ جب وہ کام بیچ میں ٹوٹ جائے تو کیا ہوتا ہے۔
ایک حقیقی worker کی ایک باری کا تصور کریں۔ یہ ایک ایجنٹ کو call کرتا ہے، ایجنٹ چند tools کو call کرتا ہے، اور وہ tools ایک database، ایک payment API، اور ایک model سے ٹکراتے ہیں۔ یہ لگاتار کئی network calls ہیں، اور ان میں سے کوئی ایک بھی ناکام ہو سکتا ہے: ایک timeout، ایک ٹوٹا ہوا connection، ایک سروس جو چند سیکنڈ کے لیے down ہو۔ بغیر کسی حفاظت کے، ایک اکیلی چھوٹی ناکامی وہ سب کچھ پھینک دیتی ہے جو worker نے ابھی کیا اور پوری باری اوپر سے دوبارہ شروع کر دیتی ہے۔
Durability اس کا علاج ہے، اور اسے صاف الفاظ میں کہنا آسان ہے: جب کچھ بیچ میں ناکام ہوتا ہے، تو وہ steps جو پہلے ہی ختم ہو چکے ختم ہی رہتے ہیں، اور worker شروع سے کرنے کے بجائے اُس نقطے سے اٹھاتا ہے جو ٹوٹا تھا۔ اعصابی نظام والی تصویر میں، یہی reflex ہے: یہ بس ہو جاتا ہے، تیزی سے، بغیر اس کے کہ ایجنٹ کو سوچنا پڑے۔
Inngest آپ کو یہ ایک tool، step.run، اور اس کے نیچے کام کرتی ایک مشینری جسے memoization کہتے ہیں، سے دیتا ہے۔ Part 2 دونوں کا احاطہ کرتا ہے، پھر وقت-پر-مبنی نسخے (step.sleep اور step.wait_for_event)، retries کیسے برتاؤ کرتے ہیں، اور step.ai helpers۔
اگر آپ سرسری دیکھ رہے ہیں: سب سے زیادہ اہم دو تصور 6 (
step.run) اور تصور 7 (memoization) ہیں۔ Part 2 میں باقی سب کچھ ان پر بنتا ہے، تو ان دونوں کو آہستہ پڑھیں۔ ایک بار جب یہ ذہن میں بیٹھ جائیں، تو تصورات 8 سے 10 تیزی سے گزرتے ہیں۔
تصور 6: step.run اور durable function model
ایک عام Python function ایک بار چلتا ہے، اوپر سے نیچے۔ اگر یہ بیچ میں crash کرے، تو آپ اوپر سے دوبارہ شروع کرتے ہیں۔ اگر یہ crash کرنے سے پہلے تین API calls کرے، تو اگلی کوشش وہ تینوں calls دوبارہ کرتی ہے، اور ان کی ادائیگی کرتی ہے، اور شاید کسی کو دوبارہ، دوہرا چارج کر دیتی ہے۔
ایک Inngest function durable ہوتا ہے۔ ہر وہ operation جسے آپ checkpoint کرنا چاہتے ہیں، step.run(name, fn, ...) میں لپیٹا جاتا ہے۔ پھر Inngest function کو ایک وقت میں ایک step چلاتا ہے۔ یہ آپ کے handler کو اوپر سے چلاتا ہے، اور جب یہ کسی ایسے step تک پہنچتا ہے جو اس نے ابھی نہیں کیا، تو اس step کو چلاتا ہے، نتیجہ محفوظ کرتا ہے، اور handler میں اوپر سے دوبارہ داخل ہوتا ہے، اس بار ہر مکمل شدہ step کا محفوظ شدہ output دوبارہ چلائے بغیر واپس کرتا ہوا۔ function وہاں تک "پہنچ جاتا ہے" جہاں اس نے چھوڑا تھا، اگلا step لیتا ہے، اور دہراتا ہے۔ (تو handler body ایک function کے لیے کئی بار چلتا ہے، فی step ایک بار، صرف تب نہیں جب کچھ ناکام ہو۔)
handler میں سرے سے دوبارہ کیوں داخل ہوں، بجائے بس وہیں سے جاری رکھنے کے جہاں یہ چھوڑا تھا؟ شروع والے دو پروگراموں کی وجہ سے۔ engine اور آپ کا function دو الگ پروگرام ہیں۔ ایک پروگرام کسی دوسرے کے code کے بیچ میں رک کر اپنی جگہ تھامے نہیں رہ سکتا۔ تو engine آپ کے function کو اسی طریقے سے چلاتا ہے جو اس کے لیے ممکن ہے: یہ آپ کے function کو web پر call کرتا ہے، اگلے نامکمل step تک چلاتا ہے، اُس step کو چلنے دیتا ہے، اور نتیجہ واپس لیتا ہے۔ پھر یہ وہ نتیجہ اپنی طرف محفوظ کرتا ہے اور اگلے step کے لیے آپ کے function کو دوبارہ call کرتا ہے، وہ سب کچھ واپس تھماتا ہوا جو یہ پہلے محفوظ کر چکا ہے۔
ENGINE YOUR FUNCTION (host)
| call: run from the top -----------> runs to step 1, does it
| <---------------------------------- returns step 1's result
stores result 1
| call again -----------> step 1 from memo, runs step 2
| <---------------------------------- returns step 2's result
stores result 2
| call again -----------> steps 1-2 from memo, runs step 3
| ...and so on, one call per step
یہی پوری مشینری ہے۔ "اوپر سے دوبارہ چلتا ہے، مکمل شدہ steps memo سے" بس engine کا آپ کے function کو فی step ایک بار call کرنا اور نتائج اپنی طرف رکھنا ہے۔ اور چونکہ نتائج engine کی طرف رہتے ہیں، ایک ختم شدہ step اُس صورت میں بھی بچ جاتا ہے جب آپ کا host درمیانِ run crash اور restart کرے۔
@inngest_client.create_function(
fn_id="customer-support-conversation",
trigger=inngest.TriggerEvent(event="customer/email.received"),
)
async def handle_email(ctx: inngest.Context) -> dict[str, str]:
customer_id = ctx.event.data["customer_id"]
# Step 1: load the customer record (one DB call)
customer = await ctx.step.run(
"load-customer", load_customer_by_id, customer_id,
)
# Step 2: load the conversation thread (one DB call)
thread = await ctx.step.run(
"load-thread", load_thread_for_customer, customer_id,
)
# Step 3: run the OpenAI Agents SDK agent (your worker).
# step.run forwards only positional args, so a call that needs keyword
# args is wrapped in a lambda (the step body becomes a no-arg callable).
response = await ctx.step.run(
"run-agent",
lambda: run_customer_support_agent(
customer=customer,
thread=thread,
email_body=ctx.event.data["body"],
),
)
# Step 4: write the draft reply to the database
await ctx.step.run(
"save-draft-reply",
lambda: save_reply(customer_id=customer_id, text=response.draft),
)
# Step 5: notify the on-call human reviewer via Slack
await ctx.step.run(
"notify-reviewer",
lambda: post_slack_for_review(response=response),
)
return {"status": "drafted", "reviewer_notified": True}
پانچ steps۔ ہر ایک آزادانہ طور پر checkpoint ہوتا ہے۔
Durability آپ کو کیا دیتی ہے، تین ناکامیوں میں جو اِسی function کو لگ سکتی ہیں:
| اگر یہ ناکام ہو | step.run کے بغیر | step.run کے ساتھ |
|---|---|---|
| ایجنٹ timeout ہو جاتا ہے (step 3) | retry customer اور thread دوبارہ load کرتا ہے اور ایجنٹ کو شروع سے دوبارہ چلاتا ہے، OpenAI tokens کی دو بار ادائیگی کرتا ہوا | steps 1-2 memo سے واپس آتے ہیں؛ صرف step 3 retry ہوتا ہے، اور Inngest عارضی error آپ کے لیے سنبھال لیتا ہے |
| process steps 3 اور 4 کے درمیان مارا جاتا ہے (deploy، restart، OOM) | ایجنٹ کا جواب کھو جاتا ہے؛ ای میل تب تک بے جواب رہتی ہے جب تک کوئی نوٹ نہ کرے | function restart کے بعد resume ہوتا ہے: steps 1-3 ملی سیکنڈوں میں memo سے واپس آتے ہیں، steps 4-5 چلتے ہیں، کسٹمر کو جواب مل جاتا ہے |
| Slack ایک 503 واپس کرتا ہے (step 5) | آپ کام کھو دیتے ہیں، یا آپ صرف Slack کے لیے retry-and-backoff ہاتھ سے لکھتے ہیں | Inngest step 5 کو backoff کے ساتھ retry کرتا ہے جب تک Slack بحال نہ ہو یا retry budget ختم نہ ہو؛ steps 1-4 ختم رہتے ہیں، مسودہ پہلے ہی محفوظ ہے |
آپ کوئی retry loops، کوئی "کیا میں نے یہ پہلے ہی کر لیا" چیک، یا اپنا کوئی state machine نہیں لکھتے۔ state machine ہے ہی step.run calls کا سلسلہ۔
step.run کا واحد اصول۔ ایک step کا دوبارہ چلانے کے لیے محفوظ ہونا ضروری ہے: اگر یہ ناکام ہو اور Inngest اسے دوبارہ چلائے، تو دوسری بار چلنا کسی چیز کو خراب نہ کرے۔
- Pure functions خودبخود محفوظ ہیں۔
- Idempotent API calls محفوظ ہیں (Stripe کا
idempotency_key، آپ کے اپنے MCP server tools): دہرانا ایک no-op ہے۔ - Non-deterministic کام پھر بھی دوبارہ چلانے کے لیے محفوظ ہے؛ آپ کو بس retry پر ایک مختلف نتیجہ مل سکتا ہے۔ ایک تازہ random ID، یا default temperature پر ایک LLM call، دوسری کوشش پر مختلف ہوگا۔ یہ ایک ایجنٹ کے جواب کے لیے ٹھیک ہے (کوئی بھی درست مسودہ چل جائے گا)۔ جب exact value کو retries کے پار مستحکم رہنا ضروری ہو، تو اسے pin کریں: ایک seed پاس کریں، یا اسے ایک بار اپنے پہلے کے step میں پیدا کریں اور واپس پڑھیں۔
Quick check. True یا False۔ (a) Inngest کے اگلے step کی طرف بڑھنے پر function body ہر بار اوپر سے دوبارہ چلتا ہے، صرف retries پر نہیں، آپ کے
step.runcalls کے درمیان موجود سادہ code (variable assignments، branching) دوبارہ چلاتا ہوا۔ (b) اگر کوئی step مکمل ہونے میں 30 سیکنڈ لے، اور function 25 سیکنڈ میں crash کرے، تو retry اُس step کو سیکنڈ 25 سے جاری رکھتا ہے۔ (c)step.runoutputs Inngest کے infrastructure میں محفوظ ہوتے ہیں، آپ کی application میں نہیں۔
جوابات: (a) True، اور یہ لوگوں کو حیران کرتا ہے: Inngest ہر step پر آپ کے handler میں اوپر سے دوبارہ داخل ہوتا ہے، مکمل شدہ steps کو memo سے چھوڑتا ہوا۔ تو step.run کے باہر کا code ایک صاف run پر کئی بار چلتا ہے، صرف retries پر نہیں۔ ایک step کے اندر کا code ایک بار چلتا ہے، پھر memo سے واپس آتا ہے۔ (Module-level imports چاہے کچھ بھی ہو ایک بار load ہوتے ہیں؛ صرف handler body دوبارہ چلتا ہے۔) یہی اصل وجہ ہے کہ کام step.run کے اندر رکھیں۔ (b) False: step.run اکائی ہے؛ اگر کوئی step رک جائے، تو retry پورا step دوبارہ چلاتا ہے۔ اگر آپ کا step اتنا طویل ہے کہ اسے restart کی اجازت نہ دی جا سکے، تو آپ اسے چھوٹے steps میں توڑ دیتے ہیں۔ (c) True: step output store Inngest کا حصہ ہے، آپ کے DB کا نہیں۔ یہی وجہ ہے کہ آپ runs کو اپنے database schema کے بدلنے کے بعد بھی replay کر سکتے ہیں۔Try with AI
With my AI coding assistant connected to the Inngest dev server MCP,
shape a customer-support worker into an Inngest durable function.
Take a Runner.run call that processes a customer email and wrap each
of these inside its own step.run:
1. Load the customer record
2. Load the related conversation thread
3. Run the agent (the OpenAI Agents SDK Runner)
4. Persist the draft reply
5. Notify the on-call reviewer
Use grep_docs to find the current Python SDK syntax. Use
invoke_function to test it with a synthetic email payload. Then
deliberately raise an exception in step 4 and use get_run_status
to confirm steps 1-3 don't re-execute on retry.
تصور 7: Memoization، resumability کے نیچے کی مشینری
تصور 6 نے کہا "وہ steps جو پہلے ہی مکمل ہو چکے دوبارہ چلنے کے بجائے اپنے محفوظ شدہ outputs واپس کرتے ہیں۔" وہ مشینری memoization ہے، اور یہ قریب سے دیکھنے کے قابل ہے کیونکہ ہر دوسرا Inngest primitive اسی پر بنا ہے۔
جب آپ await ctx.step.run("load-customer", load_customer_by_id, "c-4429") call کرتے ہیں، تو Inngest ایک memo store رکھتا ہے جو (run_id, step_name) سے keyed ہوتا ہے۔ وہی سطر مختلف برتاؤ کرتی ہے اس بات پر کہ آیا وہ key پہلے سے بھری ہوئی ہے:
- پہلی کوشش: memo خالی ہے، تو
load_customer_by_idواقعی چلتا ہے، اور Inngest اس کے واپس کردہ کو آپ کو نتیجہ تھمانے سے پہلے محفوظ کر لیتا ہے۔ - ہر بعد کا replay (Inngest اگلے step کی طرف بڑھنے پر handler میں دوبارہ داخل ہوتا ہے، اور کسی بھی retry پر دوبارہ): memo پہلے سے
load-customerتھامے ہوئے ہے، توload_customer_by_idنہیں چلتا، DB call کبھی نہیں ہوتی، اور محفوظ شدہ value ملی سیکنڈوں میں واپس آتی ہے۔
یہی وجہ ہے کہ retries سستے ہیں (مہنگا کام پہلے سے cached ہے)، یہی وجہ ہے کہ durability درست ہے (مہنگا کام کبھی دو بار نہیں ہوتا)، اور یہی وجہ ہے کہ "body اوپر سے نیچے دوبارہ چلتا ہے" ٹھیک ہے باوجود اس کے کہ یہ ضائع کرنے والا لگتا ہے: steps کے اندر کا کام دراصل دوبارہ نہیں چلتا؛ صرف steps کے درمیان کا orchestration code چلتا ہے۔
مکمل شدہ step ایک بار ادا ہوتا ہے، ہر retry پر ایک بار نہیں۔
وہ مضمر بات جو نئے صارفین کو حیران کرتی ہے۔ step.run کے باہر کا code ہر بار چلتا ہے جب Inngest handler میں دوبارہ داخل ہوتا ہے، جو فی step ایک بار ہے، صرف retries پر نہیں۔ اگر آپ یہ کریں:
async def handle_email(ctx: inngest.Context) -> dict[str, str]:
# ANTI-PATTERN: this re-runs every time Inngest advances a step. Don't do this.
expensive_thing: dict = await fetch_expensive_data(ctx.event.data["id"])
await ctx.step.run("do-something", do_something_with, expensive_thing)
return {"status": "done"}
fetch_expensive_data ہر اُس step پر دوبارہ چلتا ہے جو function لیتا ہے، بغیر کسی ناکامی کے بھی۔ یہ ایک-step مثال بھی اسے ایک صاف run پر دو بار call کرتی ہے (فی handler-دوبارہ-داخلہ ایک بار)، اور ہر step جو آپ شامل کریں ایک اور call ہے۔ تو $0.10 فی call پر یہ کسی چیز کے ٹوٹنے سے پہلے ہی پیسے ضائع کر رہا ہے، اور ایک retry یہ سب کچھ دوبارہ ادا کرتا ہے۔ علاج یہ ہے کہ مہنگی چیز کو اس کے اپنے step میں لپیٹ دیا جائے:
async def handle_email(ctx: inngest.Context) -> dict[str, str]:
expensive_thing: dict = await ctx.step.run(
"fetch-expensive-data", fetch_expensive_data, ctx.event.data["id"],
)
await ctx.step.run("do-something", do_something_with, expensive_thing)
return {"status": "done"}
اب fetch_expensive_data memoized ہے؛ retries اس کی دوبارہ ادائیگی نہیں کرتے۔
step name ہی memo key ہے۔ Python SDK کسی دہرائے گئے نام پر نہیں ٹکراتا؛ یہ انہیں call کی ترتیب سے خودبخود نمبر دے دیتا ہے (load-customer، پھر load-customer:1، پھر load-customer:2)، تو ہر ایک کو اپنا memo slot ملتا ہے۔ مگر اس پر تکیہ نہ کریں: یہ خودکار نمبر کوئی معنیٰ نہیں رکھتے، تو load-customer:7 دکھاتا ہوا ایک dashboard trace آپ کو کچھ نہیں بتاتا کہ کون سا کسٹمر، اور ایک step ڈالنا یا نکالنا ہر بعد کے نمبر کو ہلا دیتا ہے۔ اس کے بجائے ہر call کو ایک مستحکم، data سے ماخوذ نام دیں، ایک loop میں step.run(f"load-customer-{customer_id}", ...)، تاکہ memo key data سے بندھی ہو، call کی ترتیب سے نہیں۔
Predict. آپ کے function کے تین steps ہیں۔ Step 1 (
load-customer) DB calls میں $0.01 خرچ کرتا ہے اور 100ms لیتا ہے۔ Step 2 (run-agent) OpenAI tokens میں $0.20 خرچ کرتا ہے اور 12 سیکنڈ لیتا ہے۔ Step 3 (save-draft) DB calls میں $0.005 خرچ کرتا ہے اور 50ms لیتا ہے۔ Step 2 OpenAI rate limits کی وجہ سے 30% وقت ناکام ہوتا ہے؛ Inngest backoff کے ساتھ retry کرتا ہے۔ (a) تینوں کوstep.runمیں لپیٹنے اور (b) صرف step 2 کوstep.runمیں لپیٹنے کے درمیان cost کا فرق کیا ہے؟ اعتماد 1-5۔
جواب: (a) کے ساتھ، step 2 کا ایک اکیلا retry صرف step 2 کی cost ($0.20) خرچ کرتا ہے؛ step 1 memoized اور چھوڑا ہوا ہے، اور step 3 ابھی نہیں چلا۔ (b) کے ساتھ، step 1 step.run کے باہر ہے، تو یہ step 2 کے ہر retry پر دوبارہ چلتا ہے: فی retry تقریباً $0.21 (step 1 کے لیے $0.01 اور step 2 کے لیے $0.20)۔ Step 3 یہاں cost نہیں ہے، یہ ایک بار چلتا ہے، step 2 کے آخرکار کامیاب ہونے کے بعد؛ بات یہ ہے کہ کسی ناکام step سے پہلے کا کوئی بھی کام دوبارہ چلتا ہے جب تک آپ اسے نہ لپیٹیں۔ 30% retry rate کے ساتھ ایک ہزار ای میلوں پر، یہ تقریباً $3 کے ضائع شدہ step-1 DB calls ہیں، اور اصل خطرہ پیسے سے بڑا ہے: اگر step 1 کا کوئی side effect ہوتا (ایک write، ایک charge)، تو اسے step.run کے باہر چھوڑنا اُس side effect کو ہر retry پر دوبارہ کرواتا ہے۔ ہر وہ چیز جسے آپ دوبارہ نہیں چلوانا چاہتے اسے step.run میں لپیٹیں۔ مشینری سمجھ لینے کے بعد یہ اختیاری نہیں رہتا۔Try with AI
With my AI coding assistant: review the Inngest function we built
in Concept 6's Try-with-AI and identify any code BETWEEN step.run
calls that should be wrapped in its own step but isn't. Common
candidates:
- Computed values (timestamps, IDs, formatting) that we want to be
stable across retries
- Calls to logging or metrics services
- Reads from Redis, environment variables, secret managers
Then propose a refactor that moves each of these into its own step
with a meaningful name. For each, explain whether the side effect
is one you want to happen once (use step.run) or every retry
(leave it outside).
تصور 8: step.sleep اور step.wait_for_event، وقت کے ذریعے durability
کچھ کام کو انتظار کرنا پڑتا ہے۔ ایک welcome-email pipeline فوراً ایک ای میل بھیجتی ہے، پھر تین دن انتظار کرتی ہے، پھر ایک follow-up بھیجتی ہے۔ ایک refund-investigation کو کسی انسان کی منظوری کا انتظار کرنا پڑتا ہے۔ ایک trial-conversion flow 7 دن کے اندر "user upgraded to paid" کا دھیان رکھتا ہے اور جو دیکھے اس کے مطابق ایک مختلف ای میل بھیجتا ہے۔
ایک عام Python function میں، "تین دن انتظار کرو" کا مطلب ایک process کو تین دن کھلا رکھنا ہے۔ یہ ناقابلِ عمل ہے: آپ کا process restart ہوتا ہے، آپ کی hosting آپ کو 72 گھنٹوں کے بیکار compute کا بل دیتی ہے، آپ کا timer کھو جاتا ہے۔ Inngest میں، "تین دن انتظار کرو" ایک سطر ہے:
from datetime import timedelta
@inngest_client.create_function(
fn_id="trial-welcome-series",
trigger=inngest.TriggerEvent(event="user/trial.started"),
)
async def welcome_series(ctx: inngest.Context) -> dict[str, str]:
user_id = ctx.event.data["user_id"]
await ctx.step.run("send-welcome-email", send_welcome_email, user_id)
# Wait three days. The function gets paged out of memory. Nothing
# is consuming compute. Three days later, Inngest pages it back in
# and resumes execution at the next line.
await ctx.step.sleep("wait-three-days", timedelta(days=3))
await ctx.step.run("send-followup", send_followup_email, user_id)
return {"status": "completed"}
step.sleep durable ہے، اعصابی نظام آرام میں۔ function معطل ہو جاتا ہے؛ Inngest resume time محفوظ کرتا ہے؛ انتظار کے دوران کوئی compute خرچ نہیں ہوتا؛ function درست وقت پر resume ہوتا ہے، تمام پہلے کے step outputs اب بھی memoized۔ step.sleep (اور step.sleep_until) paid plans پر ایک سال تک، مفت Hobby plan پر سات دن تک انتظار کر سکتے ہیں (Inngest usage limits)۔ سات دن کی Hobby حد اس کورس کے ہر sleep کے لیے کافی کشادہ ہے۔
زیادہ طاقتور بھائی step.wait_for_event ہے۔ وقت کا انتظار کرنے کے بجائے، کسی اور event کا انتظار کریں۔ function اُس وقت تک معطل رہتا ہے جب تک کوئی ملتی جلتی event نہ آ جائے، یا جب تک آپ کا سیٹ کردہ timeout ختم نہ ہو۔ یہی Inngest کو HITL (تصور 15) اور inter-agent coordination patterns کا سب سے صاف اظہار بناتا ہے:
@inngest_client.create_function(
fn_id="refund-with-approval",
trigger=inngest.TriggerEvent(event="customer/refund.requested"),
)
async def refund_with_approval(ctx: inngest.Context) -> dict[str, str]:
request = ctx.event.data
request_id = request["request_id"]
# If amount is over $100, require approval before issuing
if request["amount_cents"] >= 10_000:
# Notify a human via Slack/email/whatever
await ctx.step.run("notify-approver", notify_human_approver, request)
# Wait for an approval event. Up to 24 hours; expires otherwise.
approval = await ctx.step.wait_for_event(
"wait-for-approval",
event="refund/approval.decided",
timeout=timedelta(hours=24),
if_exp=f"async.data.request_id == '{request_id}'",
)
if approval is None or not approval.data.get("approved"):
return {"status": "rejected_or_timeout"}
# Either it was under $100, or it was approved
refund = await ctx.step.run(
"issue-stripe-refund", call_stripe_refund_api, request,
)
return {"status": "issued", "refund_id": refund["id"]}
اوپر سے نیچے کیا ہو رہا ہے:
the function reaches wait_for_event -> it SUSPENDS (zero compute)
|
| a human sees the Slack note, clicks Approve in your admin UI
| the UI sends a refund/approval.decided event
v
Inngest matches that event to THIS waiting run (if_exp picks the right one)
|
v
the function RESUMES, with the event as the `approval` value
|
v
the refund step runs -> Stripe refund happens, after the human approved
واحد باریک حصہ بیچ کا match ہے: if_exp وہ ہے جو approval event کو اِس request کے run کو جگاتا ہے، کسی اور کے کو نہیں۔
step.sleep اور step.wait_for_event ایسے timeouts ہیں جن کی آپ ادائیگی نہیں کرتے۔ function آپ کے code میں synchronous لگتا ہے ("تین دن انتظار کرو، پھر ای میل بھیجو")، مگر runtime semantics async اور durable ہیں۔ یہ اُن دو چیزوں میں سے ایک ہے جن کے لیے Inngest مشہور ہے (دوسری durable retries)۔ اس کے بغیر، متبادل ایک queue اور ایک state machine اور ایک database اور ایک poller ہے، اور آپ تین کے بجائے ایک ہزار سطریں لکھتے۔
Quick check. تین دعوے۔ ہر ایک کو True یا False نشان زد کریں۔ (a) اگر
step.sleep30 دن کے لیے سیٹ ہو اور آپ کی سروس اُن 30 دنوں میں پانچ بار redeploy ہو، تو ایک paid plan پر sleep بلا تعطل جاری رہتا ہے۔ (b) اگرstep.wait_for_eventtimeout ہو جائے، تو function ایک exception پھینکتا ہے۔ (c) ایک ہی function میں دوstep.wait_for_eventcalls بیک وقت ایک ہی event کا انتظار کر سکتی ہیں۔
جوابات: (a) ایک paid plan پر True: sleeps Inngest کے infrastructure میں محفوظ ہوتے ہیں، آپ کی سروس کی memory میں نہیں، اس لیے redeploys انہیں نہیں کھوتے۔ tier کی حد نوٹ کریں: 30 دن کا sleep paid plan پر ٹھیک ہے مگر مفت Hobby plan کی سات دن کی sleep حد سے زیادہ ہے۔ (b) False: timeout پر، wait_for_event None واپس کرتا ہے۔ آپ کا code اسے چیک کرتا ہے اور فیصلہ کرتا ہے کہ کیا کرنا ہے (rejection، escalation، default-approval، جو بھی policy ہو)۔ (c) عام sequential code میں False: ایک function ایک wait_for_event پر پہنچتا ہے، معطل ہوتا ہے، اور اگلے تک صرف اُس وقت پہنچتا ہے جب پہلا resume ہو جائے، تو دونوں waits سلسلہ وار چلتے ہیں، اور ایک ملتی جلتی event اُسی wait کو resume کرتی ہے جو اِس وقت معطل ہو۔ یہ صرف اُس صورت اوورلیپ ہوں گے جب آپ انہیں متوازی steps کے طور پر شروع کریں، ایک ایسا pattern جو اس کورس سے باہر ہے۔ روزمرہ کا اصول: ایک event ایک منتظر نقطے کو resume کرتی ہے۔Try with AI
Build a delayed-investigation flow with my AI coding assistant.
Specification:
1. Triggered by event 'customer/refund.failed'.
2. Immediately notify the on-call human via Slack with the refund
details and a "Investigate" button.
3. Wait for the human to click the button (which fires
'customer/refund.investigation_started') for up to 4 hours.
4. If the click arrives in time: run the agent to draft an
investigation summary.
5. If 4 hours pass without a click: escalate to a senior reviewer
by firing 'customer/refund.escalated'.
Use the dev-server MCP's send_event tool to simulate the
human-click event during testing. Use get_run_status to inspect
how the suspended function shows up in the dashboard. Before
writing, use list_docs to scan the Inngest documentation tree
for the right page on wait_for_event semantics, then
read_doc on the page you find to get the exact syntax for
the if_exp filter expression.
تصور 9: Retries، error handling، dead-letter
یہ reflex قریب سے۔ Default طور پر، Inngest ناکام steps کو retry کرتا ہے۔ defaults معقول ہیں: تقریباً 4 retries exponential backoff کے ساتھ، کوششوں کے درمیان چند سیکنڈ سے چند منٹ تک۔ آخری retry کے ناکام ہونے کے بعد، run ایک failed state میں داخل ہو جاتا ہے اور inspection اور (اختیاری طور پر) replay کے لیے وہیں رہتا ہے۔ آپ اسے فی function tune کر سکتے ہیں: retries=10، یا کبھی retry نہ کرنے کے لیے retries=0۔ کسی مخصوص ناکامی (ایک declined card، ایک 401) کے لیے retries چھوڑنے کو، step کے اندر سے inngest.NonRetriableError پھینکیں، جیسا نیچے کی مثال کرتی ہے۔
@inngest_client.create_function(
fn_id="charge-customer",
trigger=inngest.TriggerEvent(event="order/checkout.completed"),
retries=2, # transient Stripe errors (503, timeout) retry twice
)
async def charge_customer(ctx: inngest.Context) -> dict[str, str]:
try:
charge = await ctx.step.run(
"call-stripe", call_stripe_charge, ctx.event.data,
)
return {"status": "charged", "charge_id": charge["id"]}
except inngest.NonRetriableError as e:
# call_stripe_charge raises NonRetriableError on a declined card, which
# tells Inngest NOT to retry the step (a decline will not become an
# approval on attempt 2). So we land here on the FIRST failure, with no
# wasted retries, mark the order, and kick off the dunning flow.
await ctx.step.run(
"mark-failed",
lambda: mark_order_failed(ctx.event.data["order_id"], reason=str(e)),
)
await ctx.step.run(
"emit-dunning-event", emit_dunning, ctx.event.data["order_id"],
)
return {"status": "card_declined"}
تین patterns اہم ہیں۔
Pattern 1: عارضی بمقابلہ مستقل ناکامیاں۔ Inngest default طور پر ہر چیز کو retry کرتا ہے، مگر کچھ errors عارضی نہیں ہوتے۔ Stripe کا ایک card-declined error retry پر دوبارہ declined ہوگا۔ آپ کے downstream API کا ایک 401-unauthorized صرف انتظار کرنے سے 200 نہیں بن جائے گا۔ آپ کے function کو انہیں خاص طور پر پکڑنا اور سنبھالنا چاہیے: اپنے DB میں لکھیں، ایک downstream event خارج کریں، صاف ستھرا واپس آئیں، تاکہ یہ نا امید کوششوں پر retry budget ضائع نہ کریں۔ Inngest کا NonRetriableError صراحتاً Inngest کو ایک پھینکے گئے exception کے لیے retries چھوڑنے کا کہتا ہے۔
Pattern 2: Step-level بمقابلہ function-level errors۔ جو step پھینکتا ہے اسے retry کیا جاتا ہے۔ step-level retries ختم ہونے کے بعد، function ناکام ہو جاتا ہے۔ کبھی آپ چاہتے ہیں کہ ایک function ایک ناکام step کے باوجود بچ نکلے: ناکامی log کریں، کام کو "partial" نشان زد کریں، جاری رکھیں۔ step.run کو try/except میں لپیٹیں۔ step پھر بھی اپنے retries لیتا ہے؛ اگر تمام retries ناکام ہوں، تو exception آپ کے catch block تک پہنچتا ہے، جہاں آپ فیصلہ کر سکتے ہیں کہ کیا کرنا ہے۔
Pattern 3: Dead-letter اور replay۔ ایک مکمل طور پر ناکام function غائب نہیں ہوتا؛ یہ dashboard کے "failed runs" view میں اپنے پورے trace، step outputs، اور exception کے ساتھ آ جاتا ہے، ایک Replay بٹن کے ساتھ۔ bug ٹھیک کریں، اسے بھیجیں، replay کریں، بغیر کسی dead-letter handler لکھے۔ (Replay اوپر سے ایک تازہ run ہے، memo-محفوظ resume نہیں، تو side-effecting steps کو idempotent رکھیں؛ تصور 14 اس کا پورا احاطہ کرتا ہے۔)
Predict. آپ کا function step 2 میں Stripe کو اور step 4 میں آپ کے customer data service کو call کرتا ہے۔ Stripe step 2 کی پہلی کوشش پر 503 (service unavailable، عارضی) واپس کرتا ہے۔ Step 2 exponential backoff کے ساتھ 4 بار retry ہوتا ہے (تقریباً 1s، 2s، 5s، 12s)؛ چوتھے retry پر، Stripe واپس آ جاتا ہے، charge کامیاب ہوتا ہے۔ اب step 4 چلتا ہے، اور data service ایک 500 کے ساتھ down ہے۔ کیا Inngest پورا function retry کرتا ہے، یا صرف step 4؟ کتنی بار؟ اعتماد 1-5۔
جواب: صرف step 4، اور اسے اپنا retry budget ملتا ہے۔ Steps retries share نہیں کرتے۔ Step 2 کے چار retries step 4 کے سے آزاد ہیں۔ Inngest step 4 کو retry کرے گا (default تقریباً 4 بار) اور اگر data service واپس آ جائے، تو step 4 مکمل ہوتا ہے، اور function کامیاب ہو جاتا ہے۔ Step 2 کا Stripe charge دوبارہ جاری نہیں ہوتا، کیونکہ step 2 کا output اس کے کامیاب retry کے بعد memoized ہو گیا تھا۔ کسٹمر بالکل ایک بار چارج ہوتا ہے چاہے function نے retries میں 20 سیکنڈ گزارے ہوں۔Try with AI
With my AI coding assistant: extend the customer-support worker
function from Concept 6 with explicit retry and failure handling.
Specification:
1. The OpenAI Agents SDK call should retry 3 times on transient
failures (rate limit, timeout), but NOT retry on a content-policy
refusal from the model.
2. The Slack notification should retry up to 10 times (Slack is
often flaky; don't lose the notification).
3. The Postgres write should retry once; if it fails again, log the
failure and continue (don't fail the whole function over a
transient DB blip).
For each step, decide what's transient vs permanent and structure
the try/except accordingly. Use grep_docs to find the Python SDK's
NonRetriableError equivalent.
تصور 10: Python میں AI calls کے لیے step.run (step.ai.wrap صرف TypeScript کے لیے ہے)
تصورات 6-9 کسی بھی side-effecting code کے لیے کام کرتے ہیں: DB writes، API calls، file writes، agent invocations، اور اس میں آپ کے LLM calls شامل ہیں۔ تو Python میں AI calls کے لیے سرخی یہ رہی، شروع میں ہی: آپ ctx.step.run استعمال کرتے رہتے ہیں۔ Inngest AI-specific step.ai primitives بھیجتا ضرور ہے، مگر Python میں وہ یا تو دستیاب نہیں یا محدود استعمال کے ہیں، اور ان کی طرف ہاتھ بڑھانا وہ عام غلط موڑ ہے جسے روکنے کے لیے یہ تصور موجود ہے۔
شروع میں ہی ایک اہم Python-بمقابلہ-TypeScript نوٹ۔ Inngest کے
step.aimodule کے دو methods ہیں، اور ان کی زبان کی سپورٹ مختلف ہے۔step.ai.infer()TypeScript اور Python دونوں میں دستیاب ہے (Python SDK v0.5+): یہ inference کو Inngest کے infrastructure پر آف لوڈ کرتا ہے اور call کو trace کرتا ہے۔step.ai.wrap()صرف TypeScript کے لیے ہے: آج کوئی Python مساوی نہیں۔ Python projects کے لیے (جیسے اس کورس کا worker)، ایک OpenAI Agents SDK call لپیٹنے کا درست patternctx.step.run(...)ہے، جو پہلے ہی آپ کو لپیٹے ہوئے step کے inputs اور outputs کی پوری durability، retries، اور observability دیتا ہے۔ آپ کو بس وہ LLM-specific prompt/response telemetry نہیں ملتی جو TypeScript کاstep.ai.wrapشامل کرتا ہے۔ (مئی 2026 تک AI Inference docs کے against verify شدہ۔)
step.run agent run کو لپیٹتا ہے، کسی ننگے model call کو نہیں۔ اس کورس میں آپ کا worker ایک OpenAI Agents SDK agent ہے، تو agent LLM اور tool calls کرتا ہے، آپ نہیں۔ آپ پورے agent run کو ctx.step.run(...) میں لپیٹتے ہیں۔ Inngest کو فرق نہیں پڑتا کہ step کے اندر کیا ہے؛ آپ کا agent بس وہ function ہے جو آپ اسے تھماتے ہیں۔ یہ step کا input اور agent کا نتیجہ ریکارڈ کرتا ہے، عارضی ناکامی پر step کو retry کرتا ہے، اور کامیابی پر اسے memoize کرتا ہے تاکہ بعد کے steps کبھی agent کی cost دوبارہ ادا نہ کریں۔
@inngest_client.create_function(
fn_id="summarize-customer-thread",
trigger=inngest.TriggerEvent(event="customer/thread.summary_requested"),
)
async def summarize_thread(ctx: inngest.Context) -> dict[str, str]:
thread = await ctx.step.run(
"load-thread", load_thread, ctx.event.data["thread_id"],
)
# The agent makes the model and tool calls internally. You wrap the whole
# AGENT RUN in step.run, so Inngest sees it as one step: it records the
# input and the agent's result, retries on a transient failure, and
# memoizes on success so later steps do not re-pay the agent's cost.
result = await ctx.step.run(
"run-agent",
lambda: run_support_agent(thread=thread),
)
return {"summary": result.summary}
dashboard اس run کو load-thread پھر run-agent کے طور پر دکھاتا ہے، ہر ایک اپنے input اور output کے ساتھ۔ واحد چیز جو آپ کو نہیں ملتی، TypeScript کے step.ai.wrap کے مقابلے میں، وہ LLM-specific telemetry ہے (token counts، model name) جو dashboard کے AI view میں الگ کر کے دکھائی جائے؛ Agents SDK کی اپنی tracing اسے سنبھال لیتی ہے۔
agent run ایک step ہے۔ چونکہ آپ نے پورے agent کو لپیٹا، اس کے اندر کے model اور tool calls الگ Inngest steps نہیں ہیں۔ اگر agent run بیچ میں ناکام ہو اور Inngest run-agent کو retry کرے، تو پورا agent شروع سے دوبارہ چلتا ہے، اُس کوشش پر پہلے سے خرچ کیے گئے tokens دوبارہ ادا کرتا ہوا۔ یہ عام طور پر ٹھیک ہے: ایک agent مسودہ دوبارہ کرنے میں سستا ہے، اور کوئی بھی درست مسودہ چل جائے گا۔ جب ایک agent run اتنا مہنگا ہو کہ آپ اسے مکمل طور پر دوبارہ نہ کرنا چاہیں، تو کام کو چھوٹے ٹکڑوں میں توڑ دیں، ہر ایک اپنا step.run (load اور retrieve اپنے steps میں، پھر ایک مختصر agent call)، تاکہ ایک retry صرف اُس ٹکڑے کو دوبارہ کرے جو ناکام ہوا۔
چونکہ step.run ہر step کے inputs اور outputs کو Inngest کے observability store میں ریکارڈ کرتا ہے، جو مواد آپ ایک step کے ذریعے گزارتے ہیں وہ محفوظ اور dashboard میں نظر آتا ہے۔ اگر آپ کے prompt میں PII (نام، ای میلیں، پتے)، secrets (API keys، internal tokens)، contractual یا financial data، یا regulated content (HIPAA، GDPR-scoped data، PCI) شامل ہو، تو خام مواد step body میں پاس نہ کریں۔ Redact کریں، hash کریں، خلاصہ کریں، یا ایک reference پاس کریں (ایک customer_id اور ticket_id، پورا ticket text نہیں) اور حساس مواد step body کے اندر اپنے authoritative store سے دوبارہ load کریں، جہاں retention اور access controls آپ کے configure کرنے کے ہیں۔ اگر آپ OpenAI Agents SDK کی اپنی tracing فعال کریں تو وہی ضبط اس پر بھی لاگو ہوتا ہے۔ step traces کو ویسے ہی سمجھیں جیسے آپ کسی بھی production log کو سمجھیں گے: default طور پر مفید، policy سے regulated۔
step.ai.infer (Python-supported، مگر محدود استعمال کا)۔ آپ شاذ و نادر ہی اس کی طرف ہاتھ بڑھائیں گے؛ اس کورس کے ہر AI call کے لیے step.run default ہے۔ اس کا واحد مقصد: اپنے process سے OpenAI کو call کرنے کے بجائے، آپ Inngest کے infrastructure سے call کرنے کو کہتے ہیں تاکہ request راستے میں ہوتے ہوئے آپ کا process deallocate ہو سکے۔ اُن serverless platforms پر جو in-flight time کا بل دیتے ہیں، اور طویل inferences (Deep Research، بڑے embedding batches) کے لیے، یہ حقیقی پیسے بچاتا ہے؛ ایک ہمیشہ-آن server پر sub-second calls کے لیے یہ بس latency بڑھاتا ہے۔ اگر آپ اسے استعمال کریں، تو اپنے version کے لیے درست signature AI Inference docs سے نکالیں؛ یہ experimental inngest.experimental.ai namespace میں رہتا ہے اور اس کورس کے build میں استعمال نہیں ہوا۔
Quick check. True یا False۔ (a) Python میں، اپنے agent run کو
ctx.step.run("run-agent", run_support_agent, ...)میں لپیٹنا اسے durable، عارضی ناکامیوں پر retried، اور کامیابی پر memoized بنا دیتا ہے۔ (b)step.ai.inferPython میں OpenAI Agents SDK کے ساتھ Inngest استعمال کرنے کے لیے ایک سخت تقاضا ہے۔ (c) ایک اکیلے OpenAI call کے لیےstep.runکوstep.ai.inferسے بدلنا ہمیشہ function کو چلانے میں سستا بنا دے گا۔
جوابات: (a) True: یہ تجویز کردہ Python pattern ہے۔ agent run step body کے اندر جاتا ہے؛ Inngest پورے step کو کام کی اکائی سمجھتا ہے۔ (b) False: زیادہ تر صورتوں کے لیے step.run کافی ہے۔ step.ai.infer serverless compute cost کے لیے ایک optimization ہے، تقاضا نہیں۔ worked example میں OpenAI Agents SDK integration سادہ step.run استعمال کرتی ہے۔ (c) False: step.ai.infer صرف اُس وقت پیسے بچاتا ہے جب (i) آپ کسی ایسے serverless platform پر ہوں جو in-flight time کا بل دیتا ہے اور (ii) call اتنا طویل ہو کہ request-offload کی بچت اضافی orchestration overhead پر غالب آ جائے۔ ہمیشہ-آن servers پر sub-second calls کے لیے، سادہ step.run جیتتا ہے۔Try with AI
With my AI coding assistant: take a customer-support agent
invocation and produce TWO versions of the Inngest function that
calls it:
Version A: The normal pattern. Wrap the Runner.run call (the whole
agent run) in step.run: durable, retried on transient failures,
memoized, with the standard step trace.
Version B: The niche exception, for comparison. step.ai.infer can
only offload ONE model call, not a whole agent, so write a SEPARATE
small function that makes a single direct OpenAI completion via
step.ai.infer (the Python-supported primitive that hands that one
call to Inngest's infrastructure to save serverless compute cost).
This is the one place you call the model directly instead of letting
the agent do it.
For each version, explain (a) what the dashboard trace shows for a
successful run, (b) what happens when the OpenAI call hits a 429
rate limit, and (c) on which kind of deployment (always-on server
vs serverless) Version B's offload saves real money.
حصہ 3: توازن اور بحالی، پروڈکشن سکیل
حصہ 1 اور 2 نے آپ کے worker کو چلتا اور crashes سے بچتا بنا دیا۔ حصہ 3 اسے حقیقی سکیل پر چلانے کے بارے میں ہے: ایک مصروف worker کو اپنے اردگرد ہر چیز کو دبا دینے سے روکنا، اور جب کوئی چیز بڑے پیمانے پر بگڑے تو تیزی سے بحال ہونا۔ پانچ تصورات، سادہ الفاظ میں:
- Concurrency اور throttling (تصور 11): قابو کریں کہ ایک ساتھ کتنے runs ہوں، اور نئے کتنی تیزی سے شروع ہوں، تاکہ events کا سیلاب ایک ہزار database connections نہ کھولے یا ایک ہی سیکنڈ میں آپ کی OpenAI rate limit عبور نہ کر جائے۔
- ترجیح اور انصاف (تصور 12): یقینی بنائیں کہ ایک گاہک جو 500 ای میلیں بھیج رہا ہو وہ باقی سب کو لائن کے پیچھے نہ دھکیل دے۔
- Batching (تصور 13): 10,000 events کو 10,000 الگ runs کے بجائے تقریباً 100 گروپ کیے گئے runs کے طور پر سنبھالیں۔
- Replay اور cancellation (تصور 14): ایک خراب deploy کے بعد، ٹھیک کیے گئے کوڈ پر اُن runs کو دوبارہ چلائیں جو ناکام ہوئے؛ یا اُس کام کو منسوخ کریں جو اب آپ نہیں چاہتے۔
- انسانی منظوری کے gates (تصور 15): کسی اہم action سے پہلے، جیسے کوئی بڑا refund، agent کو روکیں اور کسی شخص کا انتظار کریں۔
مل کر یہ ایک چلنے والے worker کو ایسے worker میں بدل دیتے ہیں جسے آپ محفوظ طریقے سے ادائیگی کرنے والے گاہکوں کے سامنے رکھ سکیں۔
تصور 11: Concurrency اور throttling
آپ کا prototype ایک منٹ میں چند ای میلیں سنبھالتا ہے اور ٹھیک ہے۔ پھر ایک مصروف صبح ایک ساتھ 1,000 بھیجتی ہے، آپ کا worker تمام 1,000 کو ایک ہی وقت میں چلانے کی کوشش کرتا ہے، اور وہ ایک ہی لمحے میں 1,000 OpenAI calls اور 1,000 database connections کھول دیتا ہے، دونوں کو ختم کرتا ہوا۔ یہ prototype اور production کے درمیان سب سے عام خلا ہے، اور حل دو چھوٹی حدیں ہیں، ہر ایک ایک لائن:
- Concurrency یہ ہے کہ کتنے runs ایک ہی وقت میں چل سکتے ہیں۔
- Throttling یہ ہے کہ نئے runs کتنی تیزی سے شروع ہونے کی اجازت رکھتے ہیں۔
from datetime import timedelta
@inngest_client.create_function(
fn_id="customer-support-conversation",
trigger=inngest.TriggerEvent(event="customer/email.received"),
concurrency=[inngest.Concurrency(limit=10)],
throttle=inngest.Throttle(limit=100, period=timedelta(minutes=1)),
)
async def handle_email(ctx: inngest.Context) -> dict[str, str]:
...
concurrency=10 کہتا ہے: کسی بھی لمحے زیادہ سے زیادہ ان میں سے 10 functions چل رہے ہوں۔ گیارہواں event queue میں انتظار کرتا ہے جب تک کہ ان 10 میں سے ایک ختم نہ ہو۔ throttle=100/minute کہتا ہے: فی منٹ زیادہ سے زیادہ 100 نئے runs شروع ہوں۔ 101واں event انتظار کرتا ہے چاہے concurrency میں گنجائش ہو۔
آپ عموماً دونوں کیوں چاہتے ہیں۔ Concurrency آپ کے downstream systems کو بیک وقت بہت زیادہ calls سے بچاتی ہے (اوپر والا 1,000-connections کا مسئلہ)۔ Throttle انہیں ایک burst سے بچاتی ہے: اگر 500 ای میلیں ٹھیک 9:00 پر آئیں، تو آپ نہیں چاہتے کہ ایک ہی سیکنڈ میں 500 runs شروع ہوں، چاہے آپ کے پاس concurrency کی گنجائش ہو؛ throttle شروعات کو پھیلا دیتی ہے۔
نازک حصہ، اور وہ وجہ کہ اکیلی concurrency cap ہمیشہ کافی نہیں: concurrency یہ محدود کرتی ہے کہ کتنے runs جاری ہیں، نہ کہ نئے کتنی تیزی سے شروع ہوتے ہیں۔ اگر آپ کے runs تیز ہیں، تو ایک خالی slot اُسی لمحے بھر جاتا ہے جب کوئی ختم ہوتا ہے۔ تو concurrency=10 پھر بھی فی سیکنڈ سینکڑوں شروعات لانچ کر سکتا ہے، جو ایک "30 requests فی منٹ" کی حد عبور کرنے کے لیے کافی سے زیادہ ہے چاہے کبھی صرف 10 ہی چل رہے ہوں۔ تو knob کو اُس حد سے ملائیں جس کی آپ حفاظت کر رہے ہیں: ایک گنتی کی حد (20-connection والا database pool) concurrency چاہتی ہے؛ ایک rate کی حد (OpenAI کی 30 فی منٹ) throttle چاہتی ہے۔ جب runs سست ہوں، تو concurrency ضمنی طور پر rate کو بھی باندھ دیتی ہے اور شاید آپ کو throttle کی ضرورت نہ ہو؛ جب runs تیز ہوں، تو صرف throttle ہی rate کو تھامتی ہے۔
Per-key concurrency. ایک واحد concurrency حد function پر عالمی طور پر لاگو ہوتی ہے۔ ایک زیادہ دلچسپ pattern per-key concurrency ہے: event کی کسی خاصیت کے لحاظ سے محدود کریں۔ آپ ایک کے بجائے caps کی ایک فہرست پاس کرتے ہیں:
concurrency=[
inngest.Concurrency(limit=10), # global cap
inngest.Concurrency(limit=2, key="event.data.customer_id"), # per-customer cap
],
یہ کہتا ہے: عالمی طور پر زیادہ سے زیادہ 10 functions چل رہے ہوں، اور ایک وقت میں فی گاہک زیادہ سے زیادہ 2۔ اگر ایک ہی گاہک ایک منٹ میں 100 ای میلیں بھیجے، تو اُن کی صرف 2 ای میلیں بیک وقت process ہوتی ہیں؛ باقی 98 پیچھے queue میں رہتی ہیں۔ دریں اثنا، دوسرے گاہکوں کی ای میلیں معمول کے مطابق بہتی ہیں؛ وہ باتونی گاہک سے رُکی نہیں رہتیں۔ یہ دو لائن کوڈ میں multi-tenant انصاف ہے۔ تصور 12 اس pattern کو مزید آگے بڑھاتا ہے۔
پورے policy کا تصور 9am کے burst کے تحت کریں: throttle یہ سست کرتی ہے کہ runs کتنی تیزی سے شروع ہوں، concurrency cap یہ تھامتی ہے کہ ایک ساتھ کتنے چلیں، اور per-customer key ایک سیلاب کو ہر slot لینے سے روکتی ہے، جبکہ باقی سب کچھ ایک durable queue میں انتظار کرتا ہے۔
کچھ بھی گرایا نہیں جاتا؛ کام queue میں جاتا ہے۔ تین knobs طے کرتے ہیں کہ کیا چلے، کتنی تیزی سے شروع ہو، اور کون انتظار کرے۔
Quick check. تین دعوے، True یا False۔ (a) اگر آپ
concurrency=10سیٹ کریں اور 1,000 events ایک ساتھ آئیں، تو ان میں سے 990 گرا دیے جاتے ہیں۔ (b) Throttling اور concurrency کی حدیں دونوں کل throughput کو کم کرتی ہیں۔ (c) Per-key concurrency کو ایک ایسی key چاہیے جو event data سے deterministic ہو۔
جوابات: (a) False: events گرائے نہیں جاتے؛ وہ queue میں جاتے ہیں۔ Inngest کی queue durable ہے؛ 990 events اُس وقت تک انتظار کرتے ہیں جب تک concurrency slots نہ کھلیں۔ (b) False. Throttling شروعات کی rate کو محدود کرتی ہے؛ concurrency جاری runs کو محدود کرتی ہے۔ کوئی بھی کام نہیں گراتی؛ دونوں یہ شکل دیتی ہیں کہ کام کب چلے۔ ایک طویل کھڑکی پر throughput بدلتی نہیں اگر آپ کا اوسط load حدوں سے کم ہو۔ ایک peak پر throughput کو شکل دی جاتی ہے: bursts کو queue جذب کر لیتی ہے۔ (c) True: key کا expression event data پر evaluate ہوتا ہے؛ اسے ایک ہی منطقی دائرے کے لیے ایک مستحکم string پیدا کرنا ہوتا ہے (customer_id ٹھیک ہے؛ current_timestamp نہیں)۔Try with AI
With my AI coding assistant: design the concurrency and throttling
policy for the customer-support worker. Constraints:
- OpenAI rate limit: 30 requests per minute, hard cap.
- Postgres connection pool: 20 max connections (the worker takes 1 per run).
- Some customers send bursts of 30+ emails in a minute (an angry
customer); these shouldn't starve other customers.
- We expect ~1,000 emails per day, with peaks around 9am and 2pm.
Propose:
1. A global concurrency value
2. A per-customer concurrency value
3. A throttle (limit and period)
For each, explain what production failure it protects against and
what the cost is (in queue latency at peak).
تصور 12: ترجیح اور انصاف، multi-tenant سکیلنگ
Concurrency کی حدیں کام کرتی ہیں۔ Per-key concurrency بنیادی انصاف کا اضافہ کرتی ہے۔ Production-grade multi-tenant systems کو مزید چاہیے: ترجیحات (Enterprise گاہکوں کو اُسی compute کے لیے شوقیہ لوگوں کے پیچھے انتظار نہیں کرنا چاہیے) اور fair-share scheduling (کوئی ایک tenant اپنی concurrency cap کے اندر بھی پورے system پر اجارہ داری نہ کر سکے)۔
ترجیح۔ Inngest ہر event پر ایک priority expression evaluate کرتا ہے؛ زیادہ priority والے runs کم priority والے runs سے پہلے queue میں چھلانگ لگاتے ہیں۔ یہ تصور 11 کے اُسی create_function پر ایک اور argument ہے:
priority=inngest.Priority(
# Higher number wins (range -600..600). The producer puts the tier's
# priority on the event directly: Enterprise = 100, Pro = 0, Free = -100.
run="event.data.tier_priority",
),
جب concurrency queue میں 50 runs انتظار کر رہے ہوں، تو Enterprise گاہکوں کے runs پہلے جاتے ہیں، پھر Pro، پھر Free۔ اُسی tier کے اندر، FIFO ترتیب لاگو ہوتی ہے۔ ترجیح concurrency یا throttle کی حدوں کو رد نہیں کرتی؛ یہ صرف یہ طے کرتی ہے کہ انتظار کرنے والے runs میں سے کس کو اگلا خالی slot ملے۔ ایک Enterprise گاہک پھر بھی slot کھلنے کا انتظار کرتا ہے؛ بس اُسے اگلا مل جاتا ہے۔
Fair-share scheduling. جب آپ کے سینکڑوں tenants ایک ہی عالمی concurrency pool کے لیے مقابلہ کر رہے ہوں، تو FIFO جمع priority کافی نہیں۔ ایک اکیلا tenant burst بھیجتا ہوا پھر بھی منٹوں کے لیے زیادہ تر slots پر قبضہ کر سکتا ہے۔ Fair-share scheduling، جو concurrency پر key parameter کے ذریعے ایک سوچی سمجھی sizing کے ساتھ نافذ ہوتی ہے، ہر tenant کو ایک ضمانتی حصہ دیتی ہے:
concurrency=[
inngest.Concurrency(limit=50), # global pool
inngest.Concurrency(limit=3, key="event.data.tenant_id"), # max 3 per tenant
],
اس کے ساتھ: کل 50 slots، کوئی tenant 3 سے زیادہ نہیں لیتا۔ اگر 20 tenants فعال ہوں، تو زیادہ سے زیادہ 60 slots کی درخواست ہے مگر صرف 50 دستیاب ہیں۔ Fair-share انہیں باری باری گزارتی ہے، ہر tenant کو کچھ حصہ ملتا ہے، کسی کو بند نہیں کیا جاتا۔
Predict. آپ کے پاس ایک customer-support function ہے جس میں
concurrency=10اور per-customerconcurrency=2ہے۔ آپ کے پاس priority بھی configured ہے: Enterprise = high، Free = low۔ 9:00am پر، queue میں ہیں: Customer A (Free) سے 5 events، Customer B (Enterprise) سے 5 events، اور ایک اکیلے نئے Customer C (Free، جس نے ابھی اپنا پہلا plan خریدا) سے 10 events۔ یہ کس ترتیب میں چلتے ہیں؟ اعتماد 1-5۔
جواب: یہ تین passes میں طے ہوتا ہے، اس ترتیب میں۔
1. per-customer cap (2 each) -> eligible pool = 2 from A, 2 from B, 2 from C (6 runs)
2. priority sorts the pool -> B's 2 first (Enterprise), then A's 2 and C's 2 (Free, FIFO)
3. fill the 10 global slots -> all 6 fit, so 6 run now; the rest wait
جیسے ہی ہر run ختم ہوتا ہے، اُس گاہک کا اگلا queued event eligible ہو جاتا ہے (pass 1)، اور اگلا خالی slot سب سے زیادہ priority والے انتظار کرنے والے کو جاتا ہے (pass 2)۔ Per-customer cap ہی وہ چیز ہے جو Customer C کے دس events کو پوری queue لینے سے روکتی ہے۔
Flow control اس کورس کی واحد جگہ ہے جہاں "چلاؤ اور دیکھو" پوری طرح سچ نہیں رہتا۔ تصور 11 اور 12 کے چار knobs میں سے، صرف concurrency مقامی dev server پر قابلِ مشاہدہ ہے: ایک burst بھیجیں اور آپ دیکھیں گے کہ بیک وقت صرف N چلتے ہیں۔ باقی تین کو آپ مقامی طور پر configure اور سمجھتے ہیں، پھر اثر Inngest Cloud (یا ایک branch deploy) میں confirm کرتے ہیں:
- Throttle ایک rate limit ہے جسے dev server نافذ نہیں کرتا، تو مقامی طور پر آپ کے runs اتنی تیزی سے شروع ہوتے ہیں جتنا وہ کر سکیں، حد سے قطع نظر۔ Config درست ہے؛ rate صرف Cloud میں کاٹتی ہے۔
- Priority اور fair-share صرف مسلسل multi-tenant مقابلے کے تحت ظاہر ہوتے ہیں، ایک بھری ہوئی queue جس میں بہت سے tenants مقابلہ کر رہے ہوں۔ مٹھی بھر test events کبھی وہ پیدا نہیں کرتے، تو وہ درست configured ہونے کے باوجود مقامی طور پر نظر نہیں آتے۔
تو ان تین کے لیے، "verified" کا مطلب ہے کہ config قبول ہو گئی اور function چلتا ہے، اور آپ behavior کے بارے میں استدلال کر سکتے ہیں۔ ایک خاموش dev server سے یہ نتیجہ نہ نکالیں کہ "کچھ نافذ نہیں ہو رہا"؛ load کے تحت حقیقی اثر Cloud میں confirm کریں۔
Try with AI
With my AI coding assistant: extend the customer-support worker
configuration with a priority and fair-share scheme. Requirements:
1. Three customer tiers: Enterprise, Pro, Free.
2. Enterprise customers should never wait more than 5 seconds at
peak load.
3. Free tier customers should get fair access: no Free customer
should be starved for more than 60 seconds, even when the
global queue is full.
4. A single noisy customer (regardless of tier) should not occupy
more than 3 slots.
Write the concurrency + priority configuration. For each line of
config, explain which requirement it satisfies.
تصور 13: Batching، کم لاگت میں بڑی مقدار کی processing
کچھ کام فطری طور پر batched ہوتا ہے۔ آپ 10,000 گاہک گفتگوؤں میں سے ہر ایک کا خلاصہ الگ سے نہیں کرتے؛ آپ LLM کو ایک وقت میں 50 کے batch کے ساتھ call کرتے ہیں۔ آپ 10,000 audit rows ایک ایک کر کے نہیں لکھتے؛ آپ انہیں ایک bulk insert میں لکھتے ہیں۔ Inngest کا batch trigger آپ کو events جمع کرنے اور ایک واحد function کو batch بطور input کے ساتھ invoke کرنے دیتا ہے۔
@inngest_client.create_function(
fn_id="batch-embed-tickets",
trigger=inngest.TriggerEvent(event="ticket/resolved"),
batch_events=inngest.Batch(
max_size=50, # invoke when 50 events accumulated, OR
timeout=timedelta(seconds=30), # invoke when 30 seconds pass, whichever first
),
)
async def batch_embed_resolved_tickets(ctx: inngest.Context) -> dict[str, int]:
# ctx.events (plural) instead of ctx.event
ticket_ids = [e.data["ticket_id"] for e in ctx.events]
tickets = await ctx.step.run(
"load-tickets", load_tickets_by_ids, ticket_ids,
)
# One embedding call for 50 tickets, not 50 calls for 1 ticket each
embeddings = await ctx.step.run(
"embed-batch", embed_texts_batch,
[t["text"] for t in tickets],
)
await ctx.step.run(
"store-embeddings", store_embeddings_batch,
ticket_ids, embeddings,
)
return {"batched": len(ctx.events)}
کیا بدلتا ہے: ctx.events ایک فہرست ہے، نہ کہ ایک واحد event۔ Function فی event ایک بار کے بجائے فی batch ایک بار چلتا ہے۔ OpenAI embedding API کو 50 single-text calls کے بجائے ایک 50-text batch کے ساتھ call کیا جاتا ہے، جو ڈرامائی طور پر سستا (آپ فی token ادا کرتے ہیں، مگر فی request overhead ختم ہو جاتا ہے) اور تیز ہے (50 کے بجائے ایک API round-trip)۔
Batching صحیح tool ہے جب کام فطری طور پر bulkable ہو (embeddings، bulk DB writes، bulk emails) اور آپ کام ہونے سے پہلے اپنے timeout جتنی تاخیر برداشت کر سکیں۔ یہ غلط tool ہے جب ہر event کو interactive جواب چاہیے ہو یا جب events کے درمیان ترتیب غیر متوقع طریقوں سے اہم ہو۔
Quick check. True یا False۔ (a) Batched functions کو پھر بھی retries اور memoization ملتی ہے؛ پورا batch بطور ایک durably memoized ہوتا ہے۔ (b) اگر batch timeout صرف 3 events جمع ہونے پر ختم ہو، تو function اُس وقت تک نہیں چلے گا جب تک اگلے 47 نہ آ جائیں۔ (c) آپ یہ محدود کرنے کے لیے کہ کتنے batches متوازی چلیں
batch_eventsکوconcurrencyکے ساتھ ملا سکتے ہیں۔
جوابات: (a) True: batch کام کی اکائی ہے؛ retries پورے batch کو اُس کے تمام events کے ساتھ دائرے میں رکھتے ہوئے دوبارہ چلاتے ہیں۔ (b) False: timeout کا تو یہی پورا مقصد ہے۔ 30 سیکنڈ کے بعد function جو کچھ جمع ہوا اُسی کے ساتھ چلتا ہے، چاہے وہ 1 event ہو۔ (c) True: یہ production pattern ہے۔ Batch جمع concurrency مل کر آپ کے downstream load کو خوبصورتی سے محدود کرتے ہیں۔Try with AI
With my AI coding assistant: write a batched Inngest function that
embeds resolved support tickets, converting a per-ticket event
handler into one batched call.
Triggers: 'ticket/resolved' event, batched at 50 events or 30 seconds.
The function should:
1. Load the ticket bodies in one query
2. Call OpenAI embeddings API with a 50-text batch (faster + cheaper)
3. Store the embeddings
4. Emit a 'ticket/embedded' event per ticket for downstream consumers
Use grep_docs to find the OpenAI batch-embedding pattern.
تصور 14: Replay اور bulk cancellation، production بحالی
کبھی کبھی ہر چیز ایک ساتھ بگڑ جاتی ہے۔ آپ نے ایک bug ship کیا؛ پچھلے چھ گھنٹوں میں ایک ہزار runs ناکام ہوئے۔ یا آپ کا downstream API 30 منٹ کے لیے بند تھا؛ اُس کھڑکی کے دوران جو کچھ بھی اسے call کرنے کی کوشش کرتا، وہ مر گیا۔ یا آپ نے ایک logic error دریافت کی اور اسے ٹھیک کرنے کے بعد ایک دن کا کام دوبارہ کرنا چاہتے ہیں۔
پہلے، وہ فرق جو ہر کسی کو لڑکھڑاتا ہے۔ Inngest آپ کو ایک ناکام step کے دوبارہ چلنے کے دو طریقے دیتا ہے، اور وہ مختلف برتاؤ کرتے ہیں:
- خودکار retry (اُسی run کے اندر)۔ جب ایک step throw کرتا ہے، تو Inngest function کو backoff کے ساتھ retry کرتا ہے، اوپر سے دوبارہ داخل ہوتا ہوا۔ مکمل شدہ steps memo سے واپس آتے ہیں اور دوبارہ execute نہیں ہوتے؛ صرف ناکام step دوبارہ چلتا ہے۔ یہ memo-محفوظ کرنے والا resume ہے، وہی جو آپ نے Quick Win میں دیکھا، اور وہی جو "step 3 پر خرچ کیے گئے $0.20 دوبارہ خرچ نہیں ہوتے" والی خاصیت کو سچ بناتا ہے۔ یہ خودکار ہے اور اصل run کے اندر ہوتا ہے۔
- Replay / Rerun (dashboard کا بٹن، بہت سے runs پر)۔ یہ آپ کے موجودہ deployed کوڈ کے ساتھ اوپر سے ایک بالکل نیا run شروع کرتا ہے، ہر step شروع سے دوبارہ execute ہوتا ہوا (ایک rerun کو ایک نیا run id ملتا ہے اور یہ پہلا step دوبارہ چلاتا ہے، نہ کہ پرانے کا resume)۔ تو عملاً پرانے run کا memo یہاں آپ کو نہیں بچاتا۔ یہ incident بحالی کے لیے ہے، نہ کہ مکمل شدہ کام چھوڑنے کے لیے۔
ان کو سیدھا رکھنا ہی پورا تصور ہے۔ Memo کا فائدہ خودکار retry میں رہتا ہے؛ Replay ایک تازہ شروعات ہے۔ نیچے کی دو قطاریں ہر path کے تحت وہی پانچ steps ہیں:
Memo آپ کو ایک run کے اندر بچاتا ہے؛ ایک idempotency key، نہ کہ memo، آپ کو reruns کے درمیان بچاتی ہے۔
دو متضاد بحالی primitives. Replay کہتا ہے "یہ کام ناکام ہوا، میں چاہتا ہوں یہ ٹھیک کیے گئے کوڈ پر دوبارہ چلے۔" Bulk cancellation کہتا ہے "یہ کام queue میں تھا مگر اب میں نہیں چاہتا یہ ہو۔" وہی dashboard surface، متضاد ارادہ۔ زیادہ تر teams کو حقیقی traffic چلانے کے پہلے تین مہینوں میں دونوں کی ضرورت پڑتی ہے۔
Replay بحالی کا primitive ہے۔ ناکام runs اپنی پوری step history، input event، اور ناکام step سے exception کے ساتھ برقرار رہتے ہیں۔ dashboard سے آپ Functions view کھولتے ہیں، ایسے function پر filter کرتے ہیں جس کے ناکام runs ہوں، ایک time window اور ایک failure pattern منتخب کرتے ہیں (کوئی مخصوص error message یا بس "تمام failures")، اور Replay پر click کرتے ہیں۔ Inngest ہر ایک کو اُس کوڈ پر جو اب deployed ہے، اوپر سے ایک تازہ run کے طور پر schedule کرتا ہے۔
Replay کے بارے میں تین چیزیں سمجھنے کی ہیں۔
- Replay آپ کا موجودہ deployed کوڈ استعمال کرتا ہے۔ اگر آپ نے runs کے ناکام ہونے اور انہیں replay کرنے کے درمیان ایک fix deploy کیا، تو replayed runs نیا کوڈ استعمال کرتے ہیں۔ یہی تو پورا مقصد ہے: ایسے runs کی آبادی لیں جو ایک bug پر مر گئے، fix ship کریں، اور انہیں سب کو بغیر ہاتھ لگائے دوبارہ چلائیں۔
- Replay ہر step دوبارہ execute کرتا ہے؛ یہ پرانے run کا memo دوبارہ استعمال نہیں کرتا۔ ایک replayed run ایک نیا run ہے، تو ہر step ٹھیک کیے گئے کوڈ پر شروع سے دوبارہ چلتا ہے۔ لاگت کے لحاظ سے، فی replayed run صرف ناکام step کی نہیں، بلکہ پورے function کی لاگت کا منصوبہ بنائیں۔ وہ چیز جو ایک replay کو دوسرا حقیقی-دنیا side effect (ایک duplicate refund، ایک duplicate email) issue کرنے سے روکتی ہے وہ memo نہیں، بلکہ اُس side effect پر ایک idempotency key ہے (تصور 4): آپ request سے ایک مستحکم key اخذ کرتے ہیں (refund کے لیے، کچھ ایسا
(order_id, request_id)) اور provider ایک تکرار کو no-op سمجھتا ہے۔ اس کورس کا کم سے کم worker اختصار کے لیے وہ key چھوڑ دیتا ہے، اس کا refund گاہک پر match کرتا ہے اور غیر مشروط لکھتا ہے، تو ایک production version کسی حقیقی پیسے کے حرکت کرنے سے پہلے ایک کا اضافہ کرے گا۔ - Replay opt-in ہے۔ ناکام runs dashboard میں اُس وقت تک بیٹھے رہتے ہیں جب تک آپ ان پر عمل نہ کریں۔ وہ ہمیشہ retry نہیں کرتے؛ وہ غائب نہیں ہوتے۔ وہ آپ کا انتظار کرتے ہیں۔
Bulk cancellation اس کا الٹ ہے۔ کبھی کبھی آپ کے پاس ہزاروں queued یا sleeping runs ہوتے ہیں جو اب آپ نہیں چاہتے: ایک campaign منسوخ ہو گئی، ایک گاہک چھوڑ گیا اور اب آپ اسے follow-up emails نہیں بھیجنا چاہتے، ایک feature واپس roll ہو گئی۔ dashboard سے آپ ایک function اور ایک time window یا event filter منتخب کرتے ہیں، اور Cancel پر click کرتے ہیں۔ match ہونے والے runs صاف ستھرے ختم ہوتے ہیں: ان کے step.sleep اور step.wait_for_event calls resume نہیں ہوتے، queued runs شروع نہیں ہوتے، جاری runs cancellation کے لیے check کرتے ہیں اور اگلے step boundary پر نکل جاتے ہیں۔ Cancellation step boundary کا احترام کرتی ہے؛ ایک جاری step.run ختم ہونے سے پہلے اُس step کو مکمل کرتا ہے جس میں وہ ہے، تو آپ کو آدھے مکمل Stripe charges یا پھٹے ہوئے DB writes نہیں ملتے۔
Replay بمقابلہ cancellation بطور ایک فیصلہ۔ جب runs کی ایک آبادی کے ساتھ کچھ غلط ہو جائے، تو ایک سوال پوچھیں: کیا میں چاہتا ہوں یہ کام کامیاب ہو یا میں چاہتا ہوں یہ نہ ہو؟ اگر کام کامیاب ہونا چاہیے (bug-fix بحالی)، تو replay۔ اگر کام نہیں ہونا چاہیے (منسوخ campaign، چھوڑ گیا گاہک، واپس roll کی گئی feature)، تو cancel۔ اگر آپ غیر یقینی ہیں (مثلاً، ناکام runs میں کچھ ایسے ہیں جنہیں آپ بحال کرنا چاہتے ہیں اور کچھ جنہیں شروع میں ہی نہیں چلنا چاہیے تھا)، تو اپنی dashboard query کو زیادہ تنگ کریں تاکہ ہر subset کو صحیح علاج ملے۔
تین patterns جو یہ عملاً ممکن بناتا ہے:
- "ہم نے ایک bug ship کیا" بحالی۔ خراب deploy کی time window میں ناکام runs تلاش کریں، bug ٹھیک کریں، fix ship کریں، failures replay کریں۔ گاہک کا تجربہ: ان کی ای میل کو ایک گھنٹہ جواب نہیں ملا مگر بالآخر مل گیا، بغیر آپ کے کوئی بحالی کوڈ لکھے۔
- "campaign منسوخ ہوئی" rollback۔ ایک welcome series جو 14 دنوں میں تین follow-up emails بھیجتی ہے؛ گاہک دن 4 پر چھوڑ جاتا ہے۔ آپ دن 7 اور دن 14 کی follow-ups نہیں بھیجنا چاہتے۔ match ہونے والے
wait-for-eventاورsleepruns کو bulk-cancel کریں۔ - "schema migration" replay۔ آپ نے بدل دیا کہ agent خلاصے کیسے formats کرتا ہے؛ آپ چاہتے ہیں کل کے tickets نئے format کے ساتھ دوبارہ خلاصہ ہوں۔ اُن runs کو تلاش کریں (کامیاب ہوں یا نہیں) اور replay کریں؛ چونکہ ایک replay اوپر سے ایک تازہ run ہے، agent ہر step نئے کوڈ پر دوبارہ چلاتا ہے، جو بالکل وہی ہے جو آپ یہاں چاہتے ہیں۔ اپنے side-effecting steps کو idempotent رکھیں تاکہ انہیں دوبارہ چلانا double-charge یا double-send نہ کرے۔
dev-server MCP بحالی کو آپ کے general agent سے باہر نکلے بغیر قابلِ رسائی بناتا ہے۔ development کے دوران آپ AI سے کہہ سکتے ہیں کہ وہ ایک ناکام run کا معائنہ کرنے کے لیے get_run_status استعمال کرے، پھر ٹھیک کیے گئے کوڈ پر event کو دوبارہ fire کر کے کام بحال کرے (اسے ایک نیا event id دیں، کیونکہ وہی id کے ساتھ دوبارہ fire کرنا تصور 4 کے idempotency semantics کے ذریعے ایک no-op تک deduplicate ہو جاتا ہے)۔ dashboard کا Rerun بٹن مساوی ایک-click path ہے۔ کسی بھی طرح آپ کو موجودہ کوڈ پر ایک تازہ run ملتا ہے، نہ کہ memo-محفوظ کرنے والا resume۔
Quick check. True یا False۔ (a) ایک dashboard Replay کام کو نئے deployed کوڈ پر دوبارہ چلاتا ہے۔ (b) ایک dashboard Replay اصل run کے کامیاب steps memo سے واپس کرتا ہے اور صرف ناکام والے کو دوبارہ چلاتا ہے۔ (c) ایک ناکام run کے اندر خودکار retry مکمل شدہ steps memo سے واپس کرتا ہے اور صرف ناکام step دوبارہ چلاتا ہے۔ (d) ایک جاری function کو bulk-cancel کرنا تیزی سے ختم کرنے کے لیے بیچ میں چلنے والے
step.runکو step کے درمیان abort کر دے گا۔
جوابات: (a) True: ایک replay اوپر سے ایک تازہ run ہے جو کچھ بھی اب deployed ہے، یہی وجہ ہے کہ یہ bug-fix بحالی کا tool ہے۔ (b) False: یہی جال ہے۔ ایک replay ایک نیا run ہے جو ہر step اوپر سے دوبارہ execute کرتا ہے، تو پرانے run کا memo منتقل نہیں ہوتا۔ جو چیز ایک replayed side effect کو دو بار fire ہونے سے روکتی ہے وہ idempotency key ہے، نہ کہ memo۔ (c) True: یہ memo-محفوظ کرنے والا path ہے، اور وہی جو آپ نے Quick Win میں دیکھا۔ مکمل شدہ step ایک کوشش پر بیٹھا رہتا ہے جبکہ ناکام step retry کرتا ہے۔ (d) False: cancellation step boundary کا احترام کرتی ہے؛ موجودہ step.run run کے ختم ہونے سے پہلے مکمل ہوتا ہے (یا ناکام)۔ یہ پھٹے ہوئے writes کو روکتا ہے۔Try with AI
Walk through a recovery scenario with my AI coding assistant:
Yesterday at 14:00 we deployed a change to the worker's agent step.
A bug in the new code made the agent step throw on every run.
From 14:00 to 18:00, 47 customer-support runs failed at that step.
At 18:30 we noticed, fixed the bug, and re-deployed.
Use the dev-server MCP's grep_docs to find Inngest's replay docs,
then:
1. Outline the exact dashboard steps to identify the 47 failed runs.
2. Explain what a dashboard Replay does for one of those runs: is it
a fresh run from the top on the fixed code, or a resume that
reuses the old run's memo? What does that mean for the cost of
replaying all 47?
3. Confirm whether the customers will see one reply or several if a
replayed run re-sends the email, and name the mechanism that
keeps it to one (hint: it is not memo).
4. Identify ONE scenario in this story where you'd prefer to
bulk-cancel instead of replay, and explain why.
تصور 15: HITL gates بذریعہ step.wait_for_event، runtime میں Invariant 1
کچھ actions اتنے اہم ہوتے ہیں کہ agent کو خود لینے دینا مناسب نہیں۔ ایک $500 refund issue کرنا، ایک قانونی notice بھیجنا، ایک account بند کرنا: آپ چاہتے ہیں agent تحقیق کرے اور action کی تجویز دے، مگر کوئی شخص اس کے حقیقت میں ہونے سے پہلے اسے منظور کرے۔ انسان کے لیے وہ توقف ایک approval gate ہے، اور یہ اس پورے system میں واحد جگہ ہے جہاں worker رُکتا ہے اور کسی کا انتظار کرتا ہے۔ (Agent Factory کی اصطلاحات میں یہ Invariant 1 ہے، انسان principal ہے: ایک اہم فیصلے پر، جو چلتا ہے وہ اُس شخص کا فیصلہ ہے، نہ کہ agent کا۔)
Inngest کا step.wait_for_event (تصور 8) اسے صاف بناتا ہے۔ agent فیصلے کے مقام تک چلتا ہے، پھر معطل ہو جاتا ہے اور ایک approval event کا انتظار کرتا ہے۔ ایک انسان اس کا جائزہ لیتا ہے (Slack، ایک admin UI، یا ای میل میں) اور approve یا reject پر click کرتا ہے؛ وہ click event کو fire کرتا ہے، function فیصلے کے ساتھ جاگتا ہے، اور عمل کرتا ہے۔ آپ کا کوڈ یہ کنٹرول کرتا ہے کہ agent کو کیا کرنے کی اجازت ہے، نہ کہ وہ کیسے سوچتا ہے۔
@inngest_client.create_function(
fn_id="refund-with-hitl-gate",
trigger=inngest.TriggerEvent(event="customer/refund.investigated"),
concurrency=[inngest.Concurrency(limit=5)],
)
async def refund_with_gate(ctx: inngest.Context) -> dict[str, str]:
request_id = ctx.event.data["request_id"]
amount_cents = ctx.event.data["amount_cents"]
# Step 1: the agent's analysis (your worker, run durably).
# Keyword-arg calls are wrapped in a lambda; step.run forwards only positional args.
analysis = await ctx.step.run(
"agent-investigates",
lambda: run_refund_investigation_agent(request_id=request_id),
)
# Step 2: if the agent thinks refund is warranted AND amount > $100,
# gate behind human approval
needs_approval = analysis.recommends_refund and amount_cents >= 10_000
if needs_approval:
await ctx.step.run(
"notify-approver",
lambda: send_slack_approval_request(
request_id=request_id,
analysis=analysis,
amount_cents=amount_cents,
),
)
# === THE HITL GATE ===
approval = await ctx.step.wait_for_event(
"wait-for-human-approval",
event="refund/approval.decided",
timeout=timedelta(hours=24),
if_exp=f"async.data.request_id == '{request_id}'",
)
if approval is None:
# Timeout: no human responded in 24h. Escalate.
await ctx.step.run(
"escalate-timeout",
lambda: escalate_to_senior_reviewer(request_id=request_id),
)
return {"status": "escalated_timeout"}
if not approval.data["approved"]:
await ctx.step.run(
"notify-rejected",
lambda: notify_customer_rejected(request_id=request_id),
)
return {"status": "rejected_by_human"}
# Either it was approved, or it didn't need approval
refund = await ctx.step.run(
"issue-refund",
lambda: call_stripe_refund(request_id=request_id, amount_cents=amount_cents),
)
await ctx.step.run(
"audit-approved-refund",
lambda: audit_refund(
request_id=request_id,
refund=refund,
approved_by="human" if needs_approval else "auto",
),
)
return {"status": "issued", "refund_id": refund["id"]}
کوڈ میں آپ کیا دیکھتے ہیں: steps کی ایک ترتیب، جس کے درمیان ایک wait_for_event ہے۔ runtime پر کیا ہو رہا ہے:
- agent چلتا ہے (step 1، durably)۔
- function فیصلہ کرتا ہے کہ آیا gate لاگو ہوتا ہے (in-code منطق، side effects سے پاک)۔
- اگر gated ہو: ایک Slack notification fire ہوتا ہے (step 2، durable)۔ function 24 گھنٹوں تک کے لیے معطل ہو جاتا ہے۔
- Slack میں ایک انسان Approve یا Reject پر click کرتا ہے۔ admin backend
refund/approval.decidedاورrequest_idکے ساتھinngest_client.sendکو call کرتا ہے۔ - Inngest event کو معطل function سے match کرتا ہے (
if_expfilter یقینی بناتا ہے کہ صرف match ہونے والے request IDs ہی match کریں)۔ function اگلی لائن پر resume ہو جاتا ہے۔ - function انسان کے فیصلے کو استعمال کرتا ہے تاکہ یا تو refund issue کرے یا rejection کا اطلاع دے۔ دونوں paths فیصلے اور منظور کرنے والے کا audit کرتے ہیں۔
یہی وہ چیز ہے جو Inngest کو ایک queue-plus-state-machine سے کیفیتی طور پر مختلف بناتی ہے۔ HITL pattern ایک primitive ہے۔ function کا کوڈ اوپر سے نیچے پڑھا جاتا ہے، gate inline کے ساتھ۔ کوئی callback نہیں، کوئی state بحالی نہیں، کوئی if state == waiting_for_approval: ... dispatching نہیں۔ runtime suspend/resume کا mechanic سنبھالتا ہے؛ آپ کا کوڈ policy کا اظہار کرتا ہے۔
agent تجویز دیتا ہے، ایک شخص فیصلہ کرتا ہے، اور انتظار کی کوئی لاگت نہیں۔
ایک بعد کا کورس Invariant 1 کو architectural طور پر بڑھاتا ہے: authored intent، spec-driven workflows، manager-of-workers layer جو طے کرتی ہے کہ کن actions پر کون سے gates لاگو ہوں۔ یہ کورس آپ کو runtime primitive دیتا ہے۔ جب وہ manager layer آئے گی، جو gate وہ نافذ کرے گی وہ بالکل یہی wait_for_event pattern ہوگی، بس fleet سکیل پر مرتب۔ ابھی primitive جاننے کا مطلب ہے کہ بعد میں architectural pattern "ایک معقول ترتیب" کے طور پر پڑھا جائے گا نہ کہ "جادو"۔
یہ وہ کلیدی پتھر ہے جسے آپ حصہ 4 کے Decision 5 میں بناتے ہیں: refund کی منظوری، durable بنائی گئی۔ یہاں کا تصور شکل ہے؛ worked example اسے ایک حقیقی needs_approval tool سے جوڑتا ہے اور ثابت کرتا ہے کہ refund بالکل ایک بار fire ہوتا ہے۔
Predict. آپ کے پاس ایک HITL gate ہے جو
timeout=timedelta(hours=24)پر سیٹ ہے۔ ایک گاہک کی refund request جمعہ کو 17:00 پر آتی ہے۔ ہفتے کے آخر میں کوئی انسان online نہیں۔ gate کا timeout ہفتہ کو 17:00 پر fire ہوتا ہے۔ آپ کا timeout handler ایک blocked refund ریکارڈ کرتا ہے۔ جائزہ لینے والا پیر کو 9:00am پر request پڑھتا ہے۔ timeline سے گزریں: ہفتے کے آخر کے دوران کتنے function runs فعال تھے؟ Inngest نے کتنے compute کا بل دیا؟ اعتماد 1-5۔
جواب: ہفتے کے آخر کے دوران صفر فعال function runs۔ function معطل تھا: Inngest نے اس کی state محفوظ کی، function کو memory سے باہر page کیا، اور یا تو event یا timeout کا انتظار کیا۔ Inngest معطل وقت کا بل نہیں دیتا۔ جب ہفتہ 17:00 آیا اور timeout fire ہوا، تو function اُن چند سو ملی سیکنڈز کے لیے resume ہوا جو blocked-refund audit row لکھنے میں لگے، پھر مکمل ہو گیا۔ یہ حقیقت کہ جائزہ لینے والا پیر تک نہیں دیکھتا، worker کی طرف سے کچھ خرچ نہیں کرتی۔ Inngest پر HITL workflows کی economics اُن polling-based queues سے ڈرامائی طور پر مختلف ہیں جو آپ کو "کیا یہ ابھی منظور ہوا؟" polling کے ہر سیکنڈ کا بل دیتی ہیں۔Try with AI
With my AI coding assistant: design a durable refund-approval gate.
Specification:
1. The agent investigates and decides a refund is warranted, but the
refund tool needs human approval before it runs.
2. The gate should:
- Notify the on-call reviewer with the agent's recommendation
- Wait up to 4 hours for the reviewer to approve or reject
- On approve: issue the refund.
- On reject: do not issue; record a blocked refund.
- On 4-hour timeout: do not issue; record a blocked refund.
3. Every branch (approve/reject/timeout) writes an audit row from a
small fixed set of action names, capturing what was decided.
Use the dev-server MCP's send_event to simulate each branch of
the reviewer's decision during testing.
حصہ 4: worked example، ایک customer-support AI Worker
یہ کورس کی ریڑھ کی ہڈی ہے: جہاں آپ حقیقت میں بناتے ہیں۔ اس سے پہلے سب کچھ model اور reference تھا۔ یہاں سے آپ حقیقی worker جوڑتے ہیں۔ پہلے worker (ایک prompt)، پھر اس کے گرد nervous system، فی prompt ایک layer۔ ہر layer اُس تصور کا نام لیتی ہے جس پر وہ کھڑی ہے، تو اگر کوئی layer ایک "کیوں" اٹھائے، تو حصہ 1-3 کا وہ تصور کھولنے والا صفحہ ہے۔ آپ اپنے general agent کو مختصر، سادہ-انگریزی prompts میں ہدایت دیتے ہیں اور وہ کوڈ لکھتا ہے۔ نیچے دکھائے گئے snippets ہر layer کی چند کلیدی لائنیں ہیں، فائلیں نہیں۔ مکمل implementation کو ایک live dev server اور ایک حقیقی model کے خلاف end-to-end چلایا گیا، تو جو شکلیں آپ دیکھتے ہیں وہی چلتی ہیں۔ اگر کوئی signature ناآشنا لگے، تو آپ کا agent موجودہ docs چیک کرتا ہے۔
پورا flow جو آپ بنانے والے ہیں، ایک ای میل سرے سے سرے تک:
a customer emails
|
v
the INNGEST ENGINE catches the event and drives your worker,
one step at a time, storing each result as it goes:
1. audit: "message received"
2. load the customer from Neon
3. YOUR AGENT drafts a reply (the thinking part; D1 makes it durable)
4. is it a refund? PAUSE for a human (waits hours, survives crashes; D5)
5. on approve: issue the refund; on reject: record it
6. audit: "reply sent"
if a step crashes, the engine re-runs only that step, never the
finished ones (D6). the same worker also wakes on a daily cron
and runs under flow-control caps (D3, D4).
افتتاحیے کی وہی دو-program تصویر، engine آپ کے agent کو چلاتا ہوا، اب حقیقی worker۔ آپ اسے ایک وقت میں ایک layer بناتے ہیں:
شکل: سات prompts، اُسی base پر جو آپ پہلے ہی سیٹ کر چکے ہیں۔
- D0 worker کو خود، standalone بناتا ہے۔
- D1 agent run کو durable بناتا ہے۔
- D2 ایک event کو اسے جگانے دیتا ہے۔
- D3 ایک روزانہ cron جوڑتا ہے جو fan out کرتا ہے۔
- D4 flow control جوڑتا ہے۔
- D5 کلیدی پتھر ہے: refunds پر ایک durable انسانی-منظوری gate۔
- D6 ثابت کرتا ہے کہ worker ایک ٹوٹے step سے بچتا ہے: مکمل کیے گئے کام کو دہرائے بغیر retry، پھر بحالی۔
agent D0 کے بعد کبھی نہیں بدلتا؛ ہر layer nervous system ہے، باہر سے جوڑی گئی۔
شروع کرنے سے پہلے۔ آپ کا environment پہلے ہی Quick Win سے سیٹ ہے: وہی
ai-agent-nervous-systemfolder کھولیں، Inngest اورneon-postgresSkills نصب، آپ کیOPENAI_API_KEYاور آپ کا NeonDATABASE_URL.envمیں، آپ کیcustomersاورaudit_logtables فراہم شدہ، اور تینوں MCP servers (Neon، Context7،inngest-dev) جُڑے ہوئے۔ صرف دو یاددہانیاں:
- dev server چل رہا ہے۔ اگر آپ نے اسے بند کیا ہو تو دوبارہ شروع کریں: اپنے الگ terminal میں
npx inngest-cli@latest dev۔ dashboardhttp://127.0.0.1:8288پر ہے۔ (جب آپ بعد میں Inngest Cloud پر deploy کریں، تو مفت Hobby tier بغیر credit card کے $0 ہے؛ اس کی حدیں حصہ 5 میں ہیں۔)- نیچے MCP calls کے لیے ایک casing نوٹ۔ dev-server tool نام
snake_caseہیں (send_event،get_run_status،invoke_function)، مگر ان کے parameterscamelCaseہیں (get_run_statusrunIdلیتا ہے،invoke_functionfunctionIdلیتا ہے)۔ Python SDK سراسرsnake_caseہے؛ صرف MCP call parameterscamelCaseہیں۔
بریف
آپ ایک چھوٹا customer-support worker بناتے ہیں اور اسے ایک nervous system دیتے ہیں۔ worker اپنے sample گاہک Neon customers table سے پڑھتا ہے (id، email، tier)، آنے والی ای میل کا ایک گرمجوش جواب draft کرتا ہے، صرف انسانی منظوری سے refund issue کر سکتا ہے، اور ہر action کے لیے Neon audit_log table میں ایک audit row لکھتا ہے، action ناموں کے ایک چھوٹے مقررہ set سے جو وہ منتخب کرتا ہے (ایک بند set، تاکہ typo ایک خاموش خراب row کے بجائے ایک بلند آواز error بن جائے)۔ پھر سات prompts اس کے گرد Inngest جوڑتے ہیں: ایک event اسے جگاتا ہے، agent call durably چلتا ہے، ایک روزانہ cron فی eligible گاہک ایک health check fan out کرتا ہے، flow control concurrency اور throttle کو محدود کرتا ہے، refund ایک durable انسانی gate پر رُکتا ہے، اور ایک replay path ناکام runs بحال کرتا ہے۔
آگے آنے والے prompts کے بارے میں ایک نوٹ۔ ہر ایک اُسی طرح لکھا گیا ہے جیسے آپ حقیقت میں ایک general agent سے کہیں گے: مختصر، سادہ، اس پر بھروسہ کرتے ہوئے کہ وہ تفصیل سنبھال لے گا۔ وہ ٹھنڈے paste کیے ہوئے کام کرتے ہیں، اور اس سے بھی بہتر اگر آپ پہلے agent سے orient ہونے کو کہیں ("read the project and tell me what you see, then ask me anything unclear before you start") جیسے جیسے فائلیں جمع ہوتی جائیں۔ prompts منزل ہیں؛ پہلے orient ہونا on-ramp ہے۔
D0: worker بنائیں، standalone
جہاں آپ ہیں: base کھلا ہے، dev server چل رہا ہے، اور آپ کا Neon store فراہم شدہ ہے، مگر ابھی کوئی worker موجود نہیں۔ یہ Decision standalone worker بناتا ہے؛ آخر تک یہ ایک sample ای میل پر چلتا ہے اور Neon میں ایک audit row لکھتا ہے۔
base پہلے ہی ایک AGENTS.md ship کرتا ہے جسے آپ کے agent نے کھولتے ہی پڑھا، تو وہ project کو جانتا ہے۔ یہی وجہ ہے کہ یہ prompts مختصر رہتے ہیں۔ اس میں سے ایک اصول جو اپنے ذہن میں رکھنے کے قابل ہے وہ پورے کورس کا architectural invariant ہے: worker کا اپنا کوڈ کبھی inngest سے import نہیں کرتا۔ agent اور اس کے tools سادہ Python رہتے ہیں؛ nervous system انہیں باہر سے لپیٹتا ہے۔ وہ علیحدگی، agent اور nervous system کو الگ رکھنا، وہی ہے جو آپ کو بعد میں Inngest کو Temporal یا Restate سے بدلنے اور worker کو اچھوتا چھوڑنے دیتی ہے۔
آپ کا Neon system of record پہلے ہی Quick Win سے فراہم شدہ ہے: customers اور audit_log tables موجود ہیں، اور DATABASE_URL آپ کے .env میں ہے۔ تو worker اُس database کو شروع سے پڑھتا اور لکھتا ہے۔ اب worker بنائیں۔ یہ paste کریں:
Build me a minimal customer-support agent with the OpenAI Agents SDK, running in a local sandbox. It reads the sample customers from my Neon
customerstable (each row has an id, email, and tier), drafts a warm reply to an incoming customer email, and can issue a refund, but the refund tool needs human approval before it runs. When an email reports a duplicate charge, an overcharge, or a failed order, the agent must actually call the refund tool, not just promise a refund in prose. Write an audit row into my Neonaudit_logtable for every action, using a small fixed set of action names and theDATABASE_URLin.env. Seed thecustomerstable with five sample rows first if it is empty. Keep it small; it exists to be wrapped, not shipped. Then run it on a sample email and show me the reply.
worker DATABASE_URL کے ذریعے Postgres تک پہنچتا ہے، کبھی Neon MCP کے ذریعے نہیں (وہ آپ کا build-time tool ہی ہے)۔ agent جو لکھتا ہے اس میں سے ایک لائن باقی کورس کے لیے کلیدی ہے، refund tool کا decorator:
@function_tool(needs_approval=True)
def issue_refund(order_id: str, amount_cents: int, reason: str) -> str:
...
needs_approval=True agent کو refund issue کرنے کے بجائے رُکنے پر مجبور کرتا ہے: run واپس آتا ہے refund کے ساتھ، ایک انسان کے فیصلہ کرنے کے لیے زیر التواء۔ یہی وہ ہک ہے جس پر D5 کلیدی پتھر لٹکتا ہے۔ (یہ floor ہر refund کو gate کرتا ہے تاکہ کلیدی پتھر سادہ رہے؛ production میں آپ صرف ایک حد سے اوپر gate کریں گے، تصور 15 کا over-$100 pattern۔ وہی wiring۔) ایک چیز جسے factored رکھیں، کیونکہ D5 اس پر ٹیک لگاتا ہے: agent اور اس کے sandbox run-config کو الگ ٹکڑوں کے طور پر بنائیں، تاکہ D5 agent کو دوبارہ بنا سکے اور resume پر sandbox دوبارہ فراہم کر سکے۔
مکمل جب: agent ایک sample ای میل پر چلتا ہے اور ایک مختصر جواب print کرتا ہے، اور Neon audit_log table میں ایک نئی row ہے (اسے console میں چیک کریں، یا اپنے agent سے کہیں کہ وہ اسے Neon tools پر دوبارہ پڑھ کر سنائے)۔ اگر ای میل ایک refund بیان کرے، تو run اسے issue کرنے کے بجائے refund tool پر رُکتا ہے؛ وہ توقف ہی پورا مقصد ہے، اور D5 اسے durable بناتا ہے۔
اس حصے کے prompts ایک frontier-class general agent (Claude Sonnet یا Opus، ایک GPT-5-class model، یا Gemini 2.5 Pro) فرض کرتے ہیں۔ جو Inngest architecture آپ سیکھ رہے ہیں (events، steps، memoization، flow control) وہ SDK-سطح کی ہے اور جو بھی model آپ کے agent کو چلائے اُس کے ساتھ قائم رہتی ہے۔ مگر build تجربہ مضبوط instruction-following پر ٹیک لگاتا ہے، خاص طور پر D5 کلیدی پتھر۔ ایک کمزور model پر، ایک prompt پر ایک سے زیادہ بار iterate کرنے اور فائلوں کے نام واضح کرنے کی توقع رکھیں۔ architecture ٹوٹی نہیں ہے؛ prompting کو بس زیادہ scaffolding چاہیے۔
D1: agent run کو durable بنائیں
جہاں آپ ہیں: ایک worker جو صرف اُس وقت چلتا ہے جب آپ اسے call کریں، run کے بیچ میں crash پر سب کچھ کھوتا ہوا۔ یہ Decision agent call کو step.run میں لپیٹتا ہے؛ آخر تک ایک مکمل run dashboard میں agent step کو memoized دکھاتا ہے۔
nervous system یہاں سے شروع ہوتا ہے: پورے agent call کو ایک واحد step.run میں لپیٹیں تاکہ یہ durable اور memoized ہو۔ یہ paste کریں:
Wrap the agent run in an Inngest durable function so it survives crashes and retries transient failures. The whole agent call goes inside a single
step.runso it is memoized. Run it in local dev mode against the Inngest dev server, with a FastAPI host. Confirm a completed run shows the agent step memoized in the dashboard.
agent call مہنگا حصہ ہے (model tokens، کئی سیکنڈ)۔ step.run کے اندر اس کا نتیجہ memoized ہوتا ہے، تو جب کوئی بعد کا step ناکام ہو اور run retry کرے، تو agent دوبارہ نہیں چلتا۔ یہی فرق ہے ایک ایسے worker میں جو ہر retry پر دوبارہ ادا اور دوبارہ عمل کرتا ہے اور ایک ایسے میں جو ہر مہنگی چیز ایک بار کرتا ہے۔ agent کو ایک سادہ (non-streamed) run کے ساتھ invoked رکھیں؛ D5 کا durable resume اس پر بنتا ہے۔
یہ دو processes کے طور پر چلتا ہے: FastAPI host، اور Inngest dev server اس کی طرف اشارہ کرتا ہوا۔ آپ کا agent دونوں شروع کرتا ہے۔
مکمل جب: dashboard function کی فہرست دیتا ہے اور ایک مکمل run agent step دکھاتا ہے۔ (آپ اسے D2 میں ایک حقیقی event سے جگاتے ہیں؛ ابھی، discoverable ہونا کافی ہے۔)
D2: اسے ایک event پر trigger کریں
جہاں آپ ہیں: durable function موجود ہے، مگر آپ اسے پھر بھی ہاتھ سے trigger کرتے ہیں اور کچھ ریکارڈ نہیں ہوتا۔ یہ Decision اسے ایک حقیقی event پر جگاتا ہے اور agent کے ہر طرف ایک audit row لکھتا ہے۔
یہ پہلی بار ہے کہ افتتاحیے کی تصویر حقیقت میں چلتی ہے۔ آپ کے worker کو call کرنے کے بجائے، ایک customer/email.received event آتا ہے، engine اسے پکڑتا ہے، اور engine آپ کے worker کو چلانے کے لیے call کرتا ہے۔ آپ یہ ریکارڈ کرنا بھی شروع کرتے ہیں کہ کیا ہوا: ایک audit row بالکل agent سے پہلے، ایک بالکل اس کے بعد۔ یہ paste کریں:
Make the worker wake on a
customer/email.receivedevent instead of being run by hand. Add an ingress audit step before the agent and a reply audit step after it. Send a test event and show me the run completing with both audit rows.
اسے مقامی طور پر test کرنے کے لیے، event خود dev-server MCP کے send_event سے بھیجیں (ایک customer/email.received event جو ای میل text اور customer id لے کر چلتا ہے)، کسی webhook کی ضرورت نہیں۔ production میں آپ اس کے بجائے اپنے ای میل provider کو ایک Inngest webhook URL کی طرف اشارہ کریں گے، جو ایک dashboard setting ہے، کوڈ نہیں۔
مکمل جب: ایک test event ایک run چلاتا ہے جو ترتیب میں تین steps (audit، agent، audit) کے ساتھ مکمل ہوتا ہے اور Neon audit_log table میں دو نئی rows، ایک agent سے پہلے اور ایک بعد۔
دو steps کیوں، ایک نہیں۔ ہر audit write اپنا step.run ہے، تو ہر ایک اپنے طور پر memoized ہوتا ہے۔ اگر reply step ناکام ہو اور run retry کرے، تو ingress row دو بار نہیں لکھی جاتی اور agent دو بار نہیں چلتا، تو audit trail retries کے دوران بالکل ایک بار رہتا ہے (وہ خاصیت جو D6 ثابت کرتا ہے)۔
D3: ایک روزانہ cron جو fan out کرتا ہے
جہاں آپ ہیں: ایک worker جسے دنیا ایک وقت میں ایک ای میل جگاتی ہے۔ یہ Decision ایک روزانہ cron جوڑتا ہے جو فی eligible گاہک ایک event fan out کرتا ہے؛ آخر تک ہر ایک کو اپنا durable child run ملتا ہے۔
scheduled کام جوڑیں: ایک روزانہ cron جو فی Pro اور Enterprise گاہک ایک health-check event fire کرتا ہے، ہر event اپنا durable run trigger کرتا ہوا۔ یہ paste کریں:
Add a daily cron that fans out one
customer/health_check.requestedevent per Pro and Enterprise customer, each one idempotency-keyed so a re-delivered cron run never double-fires. Each child event triggers its own durable run that writes one audit row. Invoke the cron manually and show me one child run per eligible customer.
دو چیزیں اس Decision کو سنبھالتی ہیں۔ fan-out ایک step کے اندر جاتا ہے (step.send_event، نہ کہ ایک ننگا client send)، تو cron کا ایک retry duplicates دوبارہ emit نہیں کرتا۔ اور ہر event کو گاہک اور cron tick سے اخذ شدہ ایک idempotency id ملتا ہے (کچھ ایسا health-{customer}-{cron_run}): اگر وہی tick دو بار پہنچے (ایک redeploy، ایک retry)، تو duplicate گر جاتا ہے، تو ہر گاہک کو اُس دن بالکل ایک check ملتا ہے۔ cron کو اپنے agent سے MCP کے invoke_function کے ساتھ invoke کریں (09:00 کا انتظار نہ کریں)۔ ایک dev خصوصیت: dev server crons صرف اُس وقت fire کرتا ہے جب وہ چل رہا ہو؛ production انہیں Inngest کے ہمیشہ-آن infrastructure پر چلاتا ہے۔
مکمل جب: parent سیکنڈوں میں مکمل ہوتا ہے اور dashboard فی eligible گاہک ایک child run دکھاتا ہے، standard-tier گاہک درست طور پر چھوڑے جاتے ہوئے۔
fan-out کیوں، ایک loop نہیں۔ parent گاہکوں کو خود process نہیں کرتا؛ یہ N events بھیجتا ہے اور واپس آ جاتا ہے۔ ہر child اپنا run ہے، الگ تھلگ، آزادانہ طور پر retryable، اپنی concurrency سے محدود۔ ایک function کے اندر ایک loop انہیں جوڑ دیتا: ایک سست گاہک باقیوں کو روک دیتا، اور ایک crash پورا batch کھو دیتا۔ fan-out وہ طریقہ ہے جس سے ایک scheduled جاگنا N آزاد durable runs بن جاتا ہے۔
D4: Flow control
پہلے ایک قدم پیچھے ہٹیں: اب تک آپ نے ایک worker جوڑا ہے، تین طریقوں سے پہنچا ہوا، سب ایک Neon store شیئر کرتے ہوئے۔ یہی وہ چیز ہے جس پر D4 caps لگاتا ہے۔
INNGEST ENGINE (routes events, runs functions, stores steps)
|
┌──────────────┼────────────────┐
v v v
an email a daily cron one run per customer
arrives fans out a the cron emitted
(D2: the check per (D3: each isolated,
email worker) customer (D3) retryable on its own)
└────────── all run in YOUR host ───────────┘
|
Neon Postgres (customers + audit_log)
ہر path کے اندر وہی agent؛ صرف یہ فرق ہے کہ دنیا اس تک کیسے پہنچتی ہے۔ اب آپ اس سب کو load کے تحت صحت مند رکھتے ہیں۔
جہاں آپ ہیں: ایک worker جو ہر ای میل سنبھالتا ہے مگر ایک burst کے تحت ان سب کو ایک ساتھ fire کر دے گا۔ یہ Decision تین flow-control policies جوڑتا ہے؛ آخر تک ایک بیس-event burst cap کے تحت queue میں جاتا ہے بغیر کسی گرائی یا duplicate row کے۔
جب پانچ سو ای میلیں 9am پر آئیں، تو worker کو ایک ساتھ پانچ سو model calls fire نہیں کرنے چاہئیں: یہ rate limit عبور کرتا ہے اور باتونی گاہک کے پیچھے ہر کسی کو بھوکا رکھتا ہے۔ ایک عالمی concurrency cap، ایک per-customer cap، اور ایک throttle جوڑیں۔ یہ paste کریں:
Add flow control to the email handler: a global concurrency cap, a per-customer concurrency key so one noisy customer can't starve the rest, and a throttle to protect the OpenAI rate limit. Fire a burst of twenty events across five customers and show me they queue under the cap and all complete with no dropped or duplicated audit rows.
تین knobs تین کام کرتے ہیں: ایک عالمی concurrency cap (ایک ساتھ کتنے runs چلیں)، ایک per-customer concurrency key (تاکہ ایک باتونی account زیادہ سے زیادہ ایک یا دو slot لے اور باقیوں کو کبھی بھوکا نہ رکھے)، اور ایک throttle (فی منٹ کتنے runs شروع ہوں)۔ throttle کو اپنی حقیقی downstream حد سے ملائیں: بریف کی OpenAI cap تقریباً 30 فی منٹ ہے، تو 30، نہ کہ ایک عام 100۔ (ایک function زیادہ سے زیادہ دو concurrency policies رکھتا ہے؛ global-plus-per-key جوڑا عام شکل ہے۔)
concurrency cap دو حدوں کی حفاظت کرتی ہے: model کی rate limit اور آپ کا Neon connection budget۔ آپ کے worker کی ایک چلتی copy پہلے ہی اپنے database connections کو محدود رکھتی ہے، کیونکہ اس میں ہر run ایک connection pool شیئر کرتا ہے۔ concurrency cap وہ چیز ہے جو کل کو سنبھالے رکھتی ہے جب آپ ایک ساتھ کئی copies چلائیں: 10 کی حد پر دس copies تقریباً 100 connections ہیں، جنہیں آپ Neon کے budget کے سامنے رکھ کر size کرتے ہیں۔ pool ایک copy کو باندھتا ہے؛ cap fleet کو باندھتی ہے۔
burst کو اپنے agent سے fire کریں: پانچ گاہکوں پر بیس customer/email.received events بذریعہ send_event۔
مکمل جب: burst cap کے تحت queue میں جاتا ہے (چلنے والی گنتی عالمی حد پر یا اس سے کم رہتی ہے، اور per-customer حد پر یا اس سے کم)، ہر run مکمل ہوتا ہے، اور audit trail میں فی event بالکل ایک row اندر اور ایک باہر ہے، بغیر کسی گرائے run کے، بغیر duplicates کے، اور بغیر کسی Neon connection error کے۔
یہ policy کیوں ہیں، کوڈ نہیں۔ اس میں سے کچھ بھی آپ کے function body میں نہیں رہتا؛ یہ configuration ہے جسے runtime نافذ کرتا ہے۔ caps کے بغیر، ایک burst یا تو ایک downstream system کو پگھلا دیتا ہے یا ایک tenant کو worker پر اجارہ داری کرنے دیتا ہے۔ وہی انصاف ہاتھ سے لکھنا ایک queue جمع ایک scheduler جمع ایک rate limiter ہے، سینکڑوں لائنیں۔ یہاں یہ تین decorator arguments ہیں۔
D5: refunds پر ایک durable انسانی-منظوری gate (کلیدی پتھر)
جہاں آپ ہیں: واپس D0 میں آپ کا agent پہلے ہی ایک refund سے پہلے رُکتا ہے، مگر وہ توقف صرف memory میں رہتا ہے۔ یہ Decision اسے ایک crash، ایک deploy، یا ایک جائزہ لینے والے سے بچاتا ہے جو گھنٹے لگائے، تاکہ refund پھر بھی بالکل ایک بار fire ہو جب وہ بالآخر منظور کریں۔
کسی بھی کوڈ سے پہلے یہاں پورا خیال ہے۔ آپ کا agent فیصلہ کرتا ہے کہ ایک refund جائز ہے، مگر اسے اُس وقت تک issue نہیں کرنا جب تک کوئی انسان ہاں نہ کہے۔ D0 کا توقف اُس فیصلے کو صرف چلتے process میں تھامتا ہے، تو ایک crash یا ایک سست جائزہ لینے والا اسے کھو دیتا ہے۔ D5 اُس توقف کو ایک durable wait میں بدل دیتا ہے: function سو جاتا ہے (کوئی لاگت نہیں) اور صرف اُس وقت جاگتا ہے جب فیصلہ آئے۔
the agent decides a refund is warranted
|
v
it PAUSES and asks a human (it does NOT issue the refund yet)
|
v
the function SLEEPS, waiting for the decision
(minutes or hours; free while it waits; survives a crash,
a deploy, a reviewer who goes to lunch)
|
v
a human clicks Approve or Reject -> sends the decision event
|
v
the function WAKES and finishes:
approved -> issue the refund (exactly once)
rejected -> no refund; record it
no answer in 4h -> no refund; record a timeout
یہ paste کریں:
Right now the agent pauses before a refund, but that pause is lost if the worker crashes or the reviewer takes hours. Make the pause survive that: when the agent stops for approval, save where it stopped, then wait up to four hours for a human's approve-or-reject for this customer. When the decision comes in, pick up exactly where the agent left off and finish, so the refund happens at most once per run. On a rejection, the reply to the customer must say the refund was declined, never that it was issued. Then prove it for me: drive a refund, show the run waiting, send an approval, and show exactly one refund row. Do it again with a rejection and show a blocked row and no refund.
وہ پوری تصویر کوڈ کی ایک لائن ہے۔ function wait_for_event پر رُکتا ہے اور جب فیصلے کا event ظاہر ہوتا ہے تو دوبارہ شروع ہوتا ہے:
decision = await ctx.step.wait_for_event(
"await-refund-approval",
event="refund/approval.decided", # what we are waiting for
timeout=datetime.timedelta(hours=4), # give up after 4 hours
if_exp=f"async.data.customer_id == '{customer_id}'", # only THIS customer's decision
)
# no decision came in 4 hours -> write a blocked-refund row and stop
# approved or rejected -> pick the agent back up and finish
وہ ایک call ہی پورا gate ہے۔ آپ کوئی queue، کوئی polling loop، اور کوئی "کیا یہ ابھی منظور ہوا؟" flags نہیں لکھتے جنہیں ہاتھ سے چیک کرنا پڑے۔ runtime آپ کے لیے توقف تھامتا ہے۔ آپ کا کوڈ بس یہ کہتا ہے کہ کس چیز کا انتظار کرنا ہے اور جواب کے ساتھ کیا کرنا ہے۔ تین چیزیں آسانی سے غلط ہو جاتی ہیں، البتہ، اور ہر ایک خاموشی سے gate کو توڑ دیتی ہے:
if_expفیصلے کو اس گاہک سے جوڑتا ہے، تو ایک گاہک کی منظوری کبھی دوسرے کا run resume نہیں کرتی۔customer_idیہاں کام کرتا ہے کیونکہ demo میں فی گاہک زیادہ سے زیادہ ایک refund زیر التواء ہوتا ہے؛ اگر ایک گاہک کے کبھی ایک ساتھ دو refunds جاری ہو سکیں، تو اس کے بجائے ایک منفردrequest_id(وہ key جو تصور 8 اور 15 استعمال کرتے ہیں) یا run id پر جوڑیں، ورنہ ایک منظوری غلط run resume کر سکتی ہے۔- جب agent resume ہو، تو اسے واپس وہ state دیں جو آپ نے محفوظ کی، نہ کہ ایک بالکل نئی گفتگو۔ اگر آپ بھول جائیں تو کیا غلط ہوتا ہے یہ ہے: ایک تازہ گفتگو یاد نہیں رکھتی کہ اس نے پہلے ہی منظوری مانگی تھی، تو resumed agent refund پر دوبارہ ٹکراتا ہے، دوبارہ منظوری مانگتا ہے، اور ہمیشہ کے لیے loop کرتا ہے۔ agent کو دوبارہ بنائیں اور اس کا run-config دوبارہ فراہم کریں، پھر اسے صرف محفوظ کی گئی state کھلائیں۔ (یہی وجہ ہے کہ D0 نے agent build اور اس کے run-config کو الگ factored رکھا؛ یہی وہ ایک تفصیل ہے جو، چھوٹ جائے، تو resume ناکام کر دیتی ہے۔)
- state محفوظ کرنا خاموشی سے آپ کا custom context گرا دیتا ہے، تو اسے ہاتھ سے واپس ڈالیں۔ یہ وہ جال ہے جو error کے بغیر ناکام ہوتا ہے۔ جب Agents SDK رُکے ہوئے run کو serialize کرتا ہے، تو یہ ایک custom run context (وہ object جس سے آپ کا refund tool customer id اور idempotency key پڑھتا ہے) منتقل نہیں کرتا؛ یہ ایک خالی محفوظ کرتا ہے اور صرف warn کرتا ہے۔ تو resume پر آپ کو وہ context خود دوبارہ فراہم کرنا ہوگا،
RunState.from_string(agent, saved_state, context_override=your_context)کے ساتھ۔ اسے چھوڑ دیں اور منظور شدہ refund tool بغیر context کے چلتا ہے: یہ خاموشی سے کوئی refund row نہیں لکھتا، جبکہ run پھر بھی کامیابی رپورٹ کرتا ہے۔ آپ "approved، مگر کوئیrefund_issuedrow نہیں" دیکھتے ہیں اور وضاحت کے لیے کچھ نہیں۔ (openai-agents0.17.x پر verified؛ serialization کے درست اصول وہ beta تفصیل ہیں جو minor versions کے درمیان بدلتی ہے، تو جب آپ بنائیں تو موجودہ Agents SDK run-state docs کے خلاف confirm کریں۔)
اسے اپنے agent سے چلائیں: ایک refund بیان کرنے والا customer/email.received event بھیجیں، run کو gate پر معطل ہوتا دیکھیں (dashboard اسے صفر compute پر WAITING دکھاتا ہے)، پھر اُس گاہک کے لیے {"approved": true, ...} لے کر چلتا ایک refund/approval.decided send_event کریں۔ اسے {"approved": false} کے ساتھ دوبارہ کریں۔
مکمل جب: منظوری پر، معطل run resume ہوتا ہے اور Neon audit_log table میں بالکل ایک refund_issued row ہے۔ rejection پر، run resume ہوتا ہے، audit میں ایک refund_blocked row ہے اور کوئی refund_issued نہیں، اور agent کا جواب انکار کی وضاحت کرتا ہے۔
gate آپ کو ایک واحد run کے اندر exactly-once دیتا ہے، اور حد بیان کرنے کے قابل ہے۔ اگر وہی refund دو runs کے ذریعے چلایا جائے (ایک دوبارہ بھیجا گیا event، ایک manual replay)، تو یہاں کوئی چیز خود سے دوسرے refund کو نہیں روکتی؛ یہ تصور 4 کی مستحکم idempotency key (یا provider کی اپنی key) کا کام ہے، request پر کلید بناتے ہوئے، بالکل جیسے وہاں refund مثال نے دکھایا۔ کم سے کم worker وہ key چھوٹا رہنے کے لیے چھوڑ دیتا ہے، تو "exactly once" کو ایک run کے خلاف ثابت کریں، اور جس لمحے ایک حقیقی refund دو بار چلایا جا سکے، تصور 4 کی key کی طرف ہاتھ بڑھائیں۔
یہ کلیدی پتھر کیوں ہے۔ ہر دوسری layer (حسیات، اضطراری اعمال، توازن) worker کو خود سے درست یا صحت مند رکھتی ہے۔ یہ وہ ہے جہاں ایک اہم action پر انسانی ذہن loop میں دوبارہ داخل ہوتا ہے، durably، جتنا بھی وقت لگے۔
D6: ثابت کریں کہ durability ایک ٹوٹے step سے بچتی ہے
جہاں آپ ہیں: ہر layer لپٹا ہوا ایک مکمل worker۔ یہ Decision اُس خاصیت کو ثابت کرتا ہے جس نے اس سب کو جائز ٹھہرایا؛ آخر تک آپ نے ایک ٹوٹے run کو اپنا ناکام step کئی بار retry کرتے دیکھا جبکہ اس کا مکمل شدہ audit step بالکل ایک بار چلا، پھر کام کو ایک تازہ run پر بحال کیا۔
ثابت کرنے کی آخری خاصیت وہ ہے جس نے اس سب کو جائز ٹھہرایا، تصور 7 کا memoization mechanic۔ آپ نے اسے وہاں سمجھا؛ اب اسے اپنے worker میں ثابت کریں۔ یہ paste کریں:
Deliberately break the agent step so it fails, fire an event, and show me Inngest retrying it while the earlier audit step stays memoized, so the failing run writes its ingress audit row exactly once across all the agent retries. Then fix the step and recover the work, and show me the recovery completing.
agent step کو جان بوجھ کر توڑیں، چند customer/email.received events fire کریں، اور ہر run کا trace پڑھیں۔ ثبوت ہر ناکام run کے اندر ہے: ingress audit step ایک مکمل کوشش دکھاتا ہے (اس کی row ایک بار لکھی گئی) جبکہ agent step کئی کوششیں دکھاتا ہے جیسے یہ backoff کے ساتھ retry کرتا ہے اور پھر ناکام ہوتا ہے، اور reply step کبھی نہیں چلتا۔ ایک کوشش پر audit step جبکہ agent step چڑھتا ہے یہ تصور 7 کا memoization ہے، اب آپ کے اپنے worker میں: ناکام run اپنی ingress row ایک بار لکھتا ہے، چاہے agent کتنی بار retry کرے۔
پھر break کو واپس کریں اور ٹھیک کیے گئے کوڈ پر event دوبارہ fire کر کے کام بحال کریں (یا، ایک حقیقی خراب-deploy batch کے لیے، dashboard کا Rerun بٹن؛ دونوں اوپر سے ایک تازہ run شروع کرتے ہیں، تصور 14)۔ یہاں وہ حصہ ہے جو لوگوں کو حیران کرتا ہے، اور یہ درست ہے، bug نہیں: بحالی ایک بالکل نیا run ہے، تو یہ اپنی اپنی ingress row لکھتا ہے۔ ایک break-then-recover کے بعد، اُس گاہک کے پاس جائز طور پر دو ingress rows ہیں، ایک ناکام run سے، ایک بحالی سے۔ Memoization ایک within-run ضمانت ہے؛ یہ کبھی دو الگ runs پر محیط نہیں ہوتی۔
مکمل جب: ناکام run کے trace میں، ingress step ایک کوشش پر بیٹھا رہا اور ایک row لکھی جبکہ agent step نے کئی کوششیں جمع کیں اور ناکام ہوا (وہ ایک-کوشش-باوجود-N-retries ہی memoization ہے)، اور پھر بحالی run ٹھیک کیے گئے کوڈ پر مکمل ہوتا ہے۔ تشخیص فی run ہے، نہ کہ فی گاہک: ایک واحد run کا trace کھولیں اور تصدیق کریں کہ ingress step ایک کوشش دکھاتا ہے۔ دو الگ runs پر دو ingress rows درست ہیں؛ ingress step کا ایک run کے اندر دو بار چلنا bug ہوگا (عموماً ایک غیر منفرد step نام)۔
یہ روشن لکیر کیوں ہے۔ ایک worker جو ایک خراب deploy پر گاہک کا کام کھو دیتا ہے بس ایک agent ہے جسے آپ call کرتے ہیں۔ ایک worker جو وہی خراب deploy لیتا ہے، بلند آواز سے ناکام ہوتا ہے، ٹوٹے step کو اُس کام کو دہرائے بغیر retry کرتا ہے جو وہ پہلے ہی ختم کر چکا، اور fix کے بعد ایک تازہ run پر صاف ستھرا بحال ہوتا ہے، وہ ایک AI Worker ہے۔
اس کم سے کم floor کے بجائے اسی nervous system کو اپنے SandboxAgent worker کی طرف اشارہ کریں؛ wrapping ایک جیسی ہے۔ اور یہ step.wait_for_event منظوری اُس کورس کے اختیاری Decision 10 کے ہاتھ سے بنے run-state table کی جگہ لیتی ہے: جو durable gate آپ نے ابھی بنایا وہی persistence layer ہے، تو آپ table delete کر سکتے ہیں۔
ابھی کیا ہوا
آپ نے ایک چھوٹا customer-support worker بنایا اور اسے ایک وقت میں ایک layer ایک nervous system دیا۔ worker کے اندرونی حصے D0 کے بعد کبھی نہیں بدلے: وہی SandboxAgent، وہی دو tools، وہی Neon Postgres audit trail۔ جو بدلا وہ اس کے گرد ہر چیز ہے۔ یہ اب ایک customer/email.received event پر اور ایک روزانہ cron پر جاگتا ہے جو فی eligible گاہک fan out کرتا ہے، durably چلتا ہے (step.run کے اندر agent call)، flow control کا احترام کرتا ہے (عالمی اور per-customer concurrency، ایک throttle)، refunds کو ایک durable انسانی منظوری پر gate کرتا ہے (step.wait_for_event)، اور ناکام runs replay کر کے ایک خراب deploy سے بحال ہوتا ہے، audit trail دکھاتا ہوا کہ کسی بھی واحد run کے اندر ہر step بالکل ایک بار fire ہوا، چاہے وہ run کتنی بار retry کرے۔
agent کوڈ وہی ہے؛ اس کی پہنچ نہیں۔ آپ نے ایک ایسے agent سے شروع کیا جسے آپ چلاتے ہیں، اسے prompt کرتے ہیں، اسے دیکھتے ہیں، اسے دوبارہ prompt کرتے ہیں۔ آپ کے پاس اب ایک worker ہے جو خود چلتا ہے: دنیا اسے جگاتی ہے، اس کے اضطراری اعمال اسے failures سے گزارتے ہیں، یہ load کے تحت اپنا توازن تھامتا ہے، اور ایک انسان صرف وہاں قدم رکھتا ہے جہاں خطرہ تقاضا کرے۔ یہی وہ لکیر ہے جو افتتاحیے نے کھینچی، ایک agent جسے آپ چلاتے ہیں اور ایک FTE جو خود چلتا ہے کے درمیان، اور آپ نے ابھی اس کے پار بنایا۔
باقی فکریں سکیل پر observability، multi-worker coordination، اور manager layer ہیں جو طے کرتی ہے کہ کون سے workers کون سا traffic سنبھالیں۔ یہ track کا اگلا کورس ہے۔ یہ کورس production-ready execution کی اکائی کا احاطہ کرتا ہے؛ اگلا ان اکائیوں کو ایک افرادی قوت میں مرتب کرتا ہے۔
حصہ 5: یہ کورس کہاں ختم ہوتا ہے
ایک AI Worker کی لاگت کی شکل
دو لاگت کی سطحیں اہم ہیں: infrastructure لاگت (Inngest، اور جو بھی store اور compute پر آپ worker چلاتے ہیں) اور inference لاگت (model tokens)۔ Infrastructure load بڑھنے کے ساتھ تقریباً ہموار رہتی ہے؛ inference خطی طور پر سکیل کرتی ہے۔ نیچے کا طریقہ وہ ہے جو سیکھنا ہے؛ کوئی بھی ڈالر کا اعداد اُسی ہفتے باسی ہو جاتا ہے جب یہ ship ہو، تو اعداد کو مثال کے طور پر سمجھیں اور budget میں کوئی اعداد ڈالنے سے پہلے موجودہ pricing صفحات چیک کریں۔
Inngest pricing. Inngest فی execution charge کرتا ہے: ہر function run، جمع ہر step-level retry، ایک execution شمار ہوتا ہے۔
| Tier | قیمت | Executions / مہینہ | بیک وقت steps | قابلِ ذکر |
|---|---|---|---|---|
| Hobby | $0 | 50,000 | 5 | 3 صارفین، 50 realtime connections، بغیر credit card |
| Pro | from $75 / month | 1,000,000 | 100+ | 1000+ realtime connections، 15+ صارفین، 7-روزہ trace retention |
| Enterprise | custom | custom | 500-50,000 | SAML / RBAC، 90-روزہ trace retention، مخصوص support |
نوٹ کریں کہ Inngest دو مختلف چیزیں meter کرتا ہے۔ ایک executions ہیں (اوپر کی table): ایک function run جمع ہر step retry۔ دوسرا events ہیں (جو آپ بھیجتے ہیں): پہلے 1-5M events فی دن شامل ہیں، اور اس سے اوپر overage تقریباً $0.000050 فی event سے شروع ہوتی ہے اور زیادہ volume پر گھٹتی ہے۔ Pro پر، 1M-execution cap سے آگے جانا فی اضافی 1M executions $50 جوڑتا ہے۔
Hobby-tier کی حدیں جو یہاں اہم ہیں۔ 5-بیک وقت-step cap کا مطلب ہے کہ چاہے آپ کوڈ میں concurrency=Concurrency(limit=10) کا اعلان کریں، platform کی account-level cap آپ کو 5 پر تھامتی ہے۔ آپ کا کوڈ production کے لیے درست ہے؛ مفت tier پر مشاہدہ کی گئی concurrency 5 ہے۔ step.sleep اور step.sleep_until بھی tier-bound ہیں: مفت Hobby plan پر سات دن تک، paid plans پر ایک سال تک (Inngest usage limits)۔
Inference لاگت غالب رہتی ہے۔ ایک عام customer-support run فی گفتگو چند ہزار سے دس ہزار model tokens استعمال کرتا ہے۔ اپنی فی-token قیمت کو اپنے tokens-per-email سے اور اپنے emails-per-day سے ضرب دیں اور آپ کے پاس وہ لائن ہے جو اہم ہے؛ زیادہ تر workers کے لیے یہ باقی سب کو بونا بنا دیتی ہے۔ یہی وہ ہے جسے آپ optimize کرتے ہیں۔ باقی سب ایک rounding error ہے۔ دو سب سے زیادہ قیمتی levers: ایک مستحکم cached prompt prefix رکھیں (تاکہ model دہرائے گئے حصے کا بل سستی cached rate پر دے، نہ کہ ہر call پر پوری قیمت)، اور آسان turns کو ایک سستے model کی طرف route کریں۔
تین Inngest-مخصوص لاگت levers جب آپ optimization zone میں ہوں:
- خالص functions کو
step.runمیں نہ لپیٹیں۔ اگر ایک function کے کوئی side effects نہیں، تو اسے durability کی ضرورت نہیں؛ اسے لپیٹنا بلا فائدہ ایک step-run charge جوڑتا ہے۔step.runکو I/O اور side effects کے لیے بچائیں۔ - bulk paths کے لیے
batch_eventsاستعمال کریں۔ ایک 50-event batch ایک function run ہے، نہ کہ 50۔ step.sleepاورstep.wait_for_eventکے ساتھ سستے میں معطل کریں۔ معطل functions معطلی کے وقت کا بل نہیں دیتے۔ ایک 3-روزہ تاخیری-followup کی لاگت اتنی ہی ہے جتنی ایک 3-سیکنڈ کی۔
سکیل پر شکل: inference وہ بل ہے جو traffic کے ساتھ بڑھتا ہے؛ Inngest، آپ کا data store، اور compute نسبتاً ہموار رہتے ہیں۔ یہاں چھپے ایک اعداد پر بھروسہ کرنے کے بجائے وہی ضرب اپنے حقیقی volume پر چلائیں۔
Swap guide: nervous system مستقل ہے، platform نہیں
یہ کورس ہر layer پر Inngest کا نام لیتا ہے۔ یہ اس لیے کہ ایک تدریسی مثال کو ٹھوس جوابات چاہئیں، نہ کہ "جو orchestrator آپ پسند کریں استعمال کریں"۔ مگر architecture کسی بھی موافق متبادل کے ساتھ کام کرتی ہے۔ پانچ swaps جنہیں کورس کا design واضح طور پر متوقع رکھتا ہے:
-
Trigger surface: Inngest events → Temporal signals، Restate handlers، AWS EventBridge + Lambda. ہر platform کے پاس "یہ کوڈ اُس وقت چلتا ہے جب یہ نامزد چیز ہوتی ہے" کا اظہار کرنے کا ایک طریقہ ہے۔ event نام، payload شکلیں، اور idempotency discipline سب منتقل ہوتے ہیں۔ جو بدلتا ہے: SDK کا decorator syntax اور dashboard۔
-
Durable execution: Inngest
step.run→ Temporal activities، Restate handlers، custom Postgres-backed state machines. ہر ایک آپ کو "اس side-effecting call کو memoize کرو، عارضی ناکامی پر retry کرو، crash کے بعد resume کرو" کے semantics دیتا ہے۔ Temporal سب سے قریبی analog ہے اور پرانا، زیادہ enterprise-آزمودہ آپشن۔ Restate نیا ترین ہے اور اس کا ذائقہ زیادہ functional-programming والا ہے۔ Custom state machines وہ ہیں جو teams لکھتی ہیں جب وہ ایک managed platform اپنا نہیں سکتیں؛ عموماً 1,000-10,000 لائنیں کوڈ جو ~70% دوبارہ بناتی ہیں جو Inngest مفت دیتا ہے۔ -
HITL primitive:
step.wait_for_event→ Temporal کاawait Workflow.execute_activity(approval_signal)، Restate کے awakeables، custom Redis/Postgres approval queues. pattern وہی ہے: function معطل ہوتا ہے، ایک بیرونی signal اسے resume کرتا ہے، audit فیصلہ پکڑتا ہے۔ Inngest کا اظہار لکھنے میں سب سے صاف ہے؛ Temporal کا زیادہ لمبا مگر بڑے سکیل پر آزمودہ۔ -
Cron scheduling: Inngest cron triggers → Kubernetes CronJobs + queue، GitHub Actions schedules، AWS EventBridge schedules. Cron triggers ایک commodity ہیں۔ Inngest کا فائدہ cron ہونا نہیں ہے؛ یہ ہے کہ cron-triggered functions کو وہی durability/replay/flow-control ملتی ہے جو event-triggered کو، خودکار۔ دوسرے platforms آپ سے وہ خود wire کراتے ہیں۔
-
Flow control: Inngest concurrency + throttle → Temporal task queues worker concurrency کے ساتھ، Redis-backed rate limiters، AWS SQS message visibility timeouts. دوسرے platforms یہ کر سکتے ہیں؛ Inngest اسے اُس configuration density کے ساتھ کرتا ہے جو ہم نے دیکھی (ایک decorator argument)۔
Dapr بطور production سکیل پر open companion. ایک زیادہ پُرعزم متبادل جس کا نام لینا قابل ہے: Dapr Agents بطور production سکیل پر Inngest کا ساختی companion، اُسی طرح جیسے OpenCode، Claude Code کا ہے۔ Dapr Agents 23 مارچ 2026 کو CNCF governance کے تحت v1.0 GA پہنچا (CNCF announcement، Dapr Agents core concepts)۔ DurableAgent production-ready class ہے؛ پرانی Agent class deprecated ہے۔ Dapr کا انتخاب کریں جب Kubernetes-native deployment اور multi-language SDKs Inngest کے مقامی dev تجربے سے زیادہ اہم ہوں۔ Inngest بہتر تدریسی tool ہے (dashboard mental model کو نظر آتا بناتا ہے)؛ Dapr بہتر سکیل tool ہے جب آپ Inngest کی tier ceilings سے ٹکرا چکے ہوں یا K8s-native multi-language deployment چاہیں۔
Inngest open source بھی ہے (github.com/inngest/inngest؛ 1.0 release نے ستمبر 2024 میں self-hosting support کا اضافہ کیا) اور Helm + KEDA کے ذریعے self-hostable۔ جو محور سکیل پر اہم ہیں وہ governance، support، اور maturity ہیں: Inngest ایک واحد vendor کے زیرِ نگرانی ہے ایک نوجوان self-hosting کہانی کے ساتھ؛ Dapr CNCF-governed ہے ایک لمبے production track record کے ساتھ۔
| اس کورس کا تصور | Inngest primitive | Dapr production analogue | تدریسی نوٹ |
|---|---|---|---|
| Scheduled کام | TriggerCron | Cron input binding / Dapr Scheduler | وہی خیال: وقت worker کو جگاتا ہے۔ Dapr کو عموماً component configuration چاہیے۔ |
| Webhook/event ingress | Inngest webhook endpoint → event | HTTP endpoint، input bindings، یا pub/sub ingress | Inngest زیادہ plumbing چھپاتا ہے؛ Dapr infrastructure کنٹرول دیتا ہے۔ |
| اندرونی events | inngest_client.send() | Dapr pub/sub | وہی event-driven mental model؛ Dapr میں broker pluggable ہے۔ |
| Fan-out | ایک event بہت سے functions trigger کرتا ہے | ایک topic/event بہت سی services consume کرتی ہیں | وہی architecture؛ Dapr broker/topic/subscriber ترتیب استعمال کرتا ہے۔ |
| Durable steps | step.run() + memoization | Dapr Workflows + activities | ملتا جلتا production مقصد، مختلف developer model۔ |
| بغیر compute انتظار | step.sleep() | Durable workflow timers | دونوں انتظار کے دوران ایک process کھلا تھامنے سے بچتے ہیں۔ |
| انسانی منظوری gate | step.wait_for_event() | Workflow external events/signals، pub/sub، actors | Inngest کا اظہار سادہ ہے؛ Dapr زیادہ composable ہے۔ |
| Retries | Function/step retries | Workflow/activity retries + resiliency policies | Dapr resiliency کو ایک runtime policy کے ساتھ ساتھ workflow behavior بھی بناتا ہے۔ |
| Dead-letter / failed runs | Inngest dashboard failed runs + replay | Broker DLQ + workflow status/restart/manual tooling | Inngest یہاں زیادہ turnkey ہے؛ Dapr زیادہ infrastructure-native ہے۔ |
| Flow control | Concurrency، throttling، priority، batching | Kubernetes scaling، app concurrency، broker controls، resiliency policies، bulk pub/sub | Dapr یہ کر سکتا ہے، مگر یہ ایک decorator argument نہیں۔ Inngest زیادہ گھنا ہے۔ |
| Stateful coordination | wait_for_event، event keys، step state | Actors + state store + workflows | Dapr Actors طویل-المدت شناخت/stateful coordination کے لیے مضبوط تر ہیں۔ |
| Agent runtime | آپ کا agent Inngest function کے اندر | DurableAgent / Dapr Agents v1.0 GA | Dapr Agents واضح طور پر agent کو workflow-backed اور resumable بناتا ہے۔ |
یہ table ایک ترجمہ گائیڈ ہے، ایک جیسی APIs کا دعویٰ نہیں۔ Inngest production pattern کو ایک compact developer تجربے کے ساتھ سکھاتا ہے: triggers، steps، waits، replay، اور flow control ایک product surface میں۔ Dapr اُسی production architecture کو distributed-systems building blocks کے ذریعے نافذ کرتا ہے: bindings، pub/sub، workflows، actors، state، resiliency، اور Kubernetes-native operations۔ تصورات براہِ راست منتقل ہوتے ہیں؛ implementation انداز بدلتا ہے۔ مئی 2026 تک Dapr کے bindings overview اور Dapr Agents core concepts کے خلاف verified۔
production سکیل پر Dapr کی طرف ہاتھ بڑھانے کی تین وجوہات:
- CNCF-governed، charter کے لحاظ سے vendor-neutral: کوئی واحد vendor platform یا اس پر آپ کے انحصار کو کنٹرول نہیں کرتا۔
- Polyglot first-class Python کے ساتھ۔ Dapr Agents Python-first ہے؛ وہی agent کوڈ JavaScript، Go، .NET، Java، یا PHP میں لکھی services کے ساتھ چل سکتا ہے بغیر کسی کے دوسرا framework سیکھے۔
- Kubernetes پر افقی طور پر سکیل ایبل، design سے۔ اپنے cluster میں چلائیں، ایک managed offering (Diagrid Catalyst) میں، یا مقامی طور پر
dapr initکے ذریعے۔ scaling کہانی ہر environment میں وہی architecture ہے۔
ایماندارانہ احتیاط: Dapr ایک getting-started platform نہیں۔ اسے production میں چلانے کا مطلب ہے Kubernetes، state store، pub/sub broker، placement service، observability، YAML components، sidecars۔ یہ ایک بڑی operational surface ہے جب آپ کا مقصد ابھی patterns سیکھنا ہی ہے، یہی وجہ ہے کہ یہ کورس Inngest پر شروع ہوتا ہے: ایک command، اور dashboard ظاہر ہو جاتا ہے۔ Dapr کی طرف اُس وقت ہاتھ بڑھائیں جب patterns بیٹھ چکے ہوں اور سوال ایسی infrastructure پر تنظیمی سکیل پر چلانے کی طرف منتقل ہو جائے جسے آپ کنٹرول کرتے ہیں۔
تصورات پہلے Inngest اور OpenAI Agents SDK پر سیکھیں: تیز feedback loop، کم سے کم infrastructure، patterns پر توجہ۔ جب آپ اُس سکیل تک پہنچیں جہاں Kubernetes governance، polyglot teams، یا vendor-neutrality ناقابلِ مذاکرہ ہو جائیں، تو وہی architectural patterns اوپر کی translation table کو اپنی key بنا کر Dapr پر اٹھ جاتے ہیں۔ patterns منتقل ہوتے ہیں؛ substrate بدلتا ہے؛ جو آپ نے اس کورس میں سیکھا وہ کلیدی علم رہتا ہے۔
یہ کورس کیا (ابھی) احاطہ نہیں کرتا
جو worker آپ نے بنایا وہ thesis کے بیان کردہ سات Invariants میں سے چار کو پورا کرتا ہے۔ بالخصوص: یہ ایک engine پر چلتا ہے (Invariant 4، SandboxAgent)، ایک system of record کے خلاف (Invariant 5، audit trail)، دنیا کے اسے call کر سکنے کے ساتھ (Invariant 7، وہ triggers جو آپ نے جوڑے)، اور ایک gated فیصلے پر انسان کے principal ہونے کے ساتھ (Invariant 1، جزوی: runtime mechanism یہاں ہے، وسیع تر architectural pattern بعد میں)۔ باقی تین Invariants، اور وہ وسیع تر architecture جو workers سے ایک افرادی قوت بناتی ہے، بعد کے کورسز ہیں۔ ہر ایک پر ایک bullet:
- Invariant 2: ہر انسان کو ایک delegate چاہیے۔ کنارے پر ایک ذاتی agent جو آپ کا context تھامتا ہے، آپ کے فیصلے کی نمائندگی کرتا ہے، اور افرادی قوت کو کام broker کرتا ہے۔ thesis موجودہ تحقق کے طور پر OpenClaw کا نام لیتا ہے۔
- Invariant 3: افرادی قوت کو ایک manager چاہیے۔ ایک orchestrator جو کام تفویض کرتا ہے، budgets نافذ کرتا ہے، execution کا audit کرتا ہے، hiring کو ایک callable صلاحیت کے طور پر بے نقاب کرتا ہے۔ thesis Paperclip کا نام لیتا ہے۔
- Invariant 6: افرادی قوت policy کے تحت قابلِ توسیع ہے۔ ایک meta-layer جہاں ایک مجاز agent ایک prompt پیدا کرتا ہے، ایک runtime فراہم کرتا ہے، اور ایک نیا worker رجسٹر کرتا ہے، بغیر کسی انسان کو جگائے۔ Claude Managed Agents ایک تحقق ہے۔
ایک واحد worker events پر جاگتا، durably چلتا، اور انسانوں پر gate کرتا ہوا، اس architecture کی سب سے چھوٹی اکائی ہے جو یہ کورس سکھاتا ہے۔ اگلا کورس اُس worker کو ایک افرادی قوت میں بڑھاتا ہے: ایک manager کے زیرِ ترتیب کئی workers، طلب پر قابلِ توسیع، triggers سے جاگے ہوئے، spec سے زیرِ نظم۔ وہی OpenAI Agents SDK بنیاد، وہی audit عادت، وہی Inngest nervous system۔ architecture مستقل ہے۔
اس میں واقعی ماہر کیسے بنیں
اس فوری کورس کو پڑھنا آپ کو AI Workers بنانے میں ماہر نہیں بناتا۔ اسے استعمال کرنا بناتا ہے۔ آپ worker بنا کر شروع کرتے ہیں، جیسے جیسے اسے لپیٹتے ہیں رگڑ محسوس کرتے ہیں، اور رگڑ کے ہر ٹکڑے کو یہ سکھانے دیتے ہیں کہ وہ کس تصور سے تعلق رکھتا ہے۔
اس کورس کا نقشہ:
- "میرا function event آنے پر کیوں نہیں fire ہوتا؟" → event نام میں typo یا namespace کا عدم مطابقت (تصور 3)۔ اپنے
TriggerEventمیں event نام string کاinngest_client.sendوالے سے byte-for-byte موازنہ کریں۔ - "میرا function وہی منطقی event کے لیے دو بار کیوں fire ہوا؟" → غائب idempotency key (تصور 4)۔ event میں ایک deterministic seed کے ساتھ ایک
id=جوڑیں۔ - "میرے function نے deploy کے بعد 'کام کیوں کھویا'؟" → کام کرتا ہوا کوڈ
step.runسے باہر (تصور 7)۔ I/O اور side effects کو نامزد steps میں لپیٹیں۔ - "گاہک سے دو بار charge کیوں ہوا؟" → Stripe call
step.runسے باہر تھا، یا step نام منفرد نہیں تھا (تصور 6 اور 7)۔ call کو ایک نامزدstep.runمیں منتقل کریں؛ step نام کو function کے اندر عالمی طور پر منفرد بنائیں۔ - "OpenAI 9am peak پر 429 errors کیوں دیتا ہے؟" → غائب throttle (تصور 11)۔
throttle=Throttle(limit=N, period=timedelta(minutes=1))جوڑیں۔ - "ایک گاہک کے bursts دوسرے گاہکوں کو کیوں بھوکا رکھتے ہیں؟" → غائب per-key concurrency (تصور 12)۔ ایک دوسرا
Concurrency(limit=2, key="event.data.customer_id")جوڑیں۔ - "میرا HITL gate ہفتے کے آخر میں خاموشی سے کیوں fire ہوا؟" → غائب timeout handler جو audit میں لکھے (تصور 15)۔
approval is Noneپر branch کریں اور audit row واضح طور پر لکھیں۔
architecture کو ایک وقت میں ایک ٹکڑا بنائیں۔ یہی وجہ ہے کہ حصہ 4 سات prompts ہے، نہ کہ ایک۔ worker بنائیں (D0)۔ agent کو step.run میں لپیٹیں (D1) اور دیکھیں کہ کیا بدلتا ہے جب آپ جان بوجھ کر run کے بیچ میں crash کریں۔ اسے ایک event پر جگائیں (D2)۔ cron fan-out جوڑیں (D3)، پھر flow control (D4) جب آپ واقعی ایک rate limit سے ٹکرا چکے ہوں، پھر durable منظوری gate (D5) جب ایک اہم action کو واقعی ایک انسان چاہیے ہو۔ ہر layer اپنا سیکھنا ہے۔ ایک بڑے rewrite میں ملا کر، وہ ایک دیوار ہیں۔
جو discipline یہ کورس سکھاتا ہے (events پر جاگو، durably چلو، انسانوں پر gate کرو، bugs پر replay کرو) وہ architectural invariant ہے۔ جو بھی platform اسے نافذ کرے، وہ چار-خاصیتی معاہدہ ہی ہے جس کے لیے آپ واقعی عہد کر رہے ہیں۔ یہ Lindy شرط ہے: آپ اُن حصوں پر بناتے ہیں جو دیر پا رہے، سادہ functions، SQL، ایک typed زبان، ایک event bus، نہ کہ اس موسم کا wrapper۔ product قابلِ تبدیل ہے؛ discipline نہیں۔
فوری حوالہ
بیانیہ کورس اور during-build حوالے کے درمیان ایک علیحدگی۔ نیچے کے sections تلاش کرنے کے لیے ہیں، اوپر سے نیچے پڑھنے کے لیے نہیں۔ ہر تصور کا ایک-لائن خلاصہ افتتاحیے کی collapsed cheat sheet میں ہے؛ یہ section during-build تشخیص، دو decision trees، اور file layout ہے۔
Decision tree: trigger surface چنیں
جب دنیا میں کوئی نئی چیز ہو، تو جگانا کہاں سے آتا ہے؟
- ایک بیرونی system نے ہمیں ایک HTTP request بھیجی۔ → Webhook trigger۔ source کو Inngest dashboard میں configure کریں؛ payload کو transform کے ذریعے دوبارہ شکل دیں؛ نتیجے میں آنے والے event کو consume کریں۔
- ایک schedule کہتا ہے کہ وقت ہو گیا۔ → Cron trigger۔
TriggerCron(cron="...")۔ UTC استعمال کریں؛ production crons تب بھی fire ہوتے ہیں جب آپ کی service deploy کے بیچ میں ہو۔ - ایک اور Inngest function نے اپنے run کے دوران ایک event emit کیا۔ → Event trigger۔
TriggerEvent(event="ns/name.subtype")۔ ایک یا کئی functions کو وہی نام subscribe کریں۔ - ایک interactive صارف فوری جواب کا انتظار کر رہا ہے۔ → یہ Inngest trigger نہیں۔ request/response کو اپنے عام web endpoint میں رکھیں؛ اگر جواب میں بھاری کام شامل ہو، تو request کے اندر سے ایک event fire کریں اور فوراً واپس آ جائیں، Inngest کو کام asynchronously سنبھالنے دیتے ہوئے۔
Decision tree: step primitive چنیں
دیا گیا کہ ایک function چل رہا ہے اور آپ کو کچھ کرنا ہے، تو آپ کون سی step.* call کی طرف ہاتھ بڑھاتے ہیں؟
- ایک side-effecting call (API، DB، file write، agent invocation)۔ →
ctx.step.run("name", fn, ...)۔ default۔ کامیابی پر memoized، عارضی ناکامی پر retried۔ - ایک طویل-چلنے والی OpenAI call ایک serverless platform پر جو in-flight time کا بل دیتا ہے۔ →
ctx.step.ai.infer(...)۔ inference کو Inngest کے infrastructure پر offload کرتا ہے تاکہ آپ کا function process deallocate ہو سکے۔ - جاری رکھنے سے پہلے ایک مقررہ مدت کا انتظار کریں۔ →
ctx.step.sleep("name", timedelta(...))۔ durable؛ انتظار کے دوران صفر compute (مفت plan پر سات دن تک، paid پر ایک سال)۔ - ایک بیرونی event کا انتظار کریں (انسانی منظوری، sibling-function تکمیل)۔ →
ctx.step.wait_for_event("name", event="...", timeout=..., if_exp=...)۔ durable؛ event آنے پر resume ہوتا ہے یا timeout پرNoneواپس کرتا ہے۔ - خالص deterministic computation (ایک string formatting، ایک تاریخ computing)۔ → بس کوڈ لکھیں۔ کسی
step.runکی ضرورت نہیں؛ کوئی charge نہیں۔
File-location فوری-حوالہ
ایک flat project، چار فائلیں، کوئی src/ nesting نہیں:
ai-agent-nervous-system/
├── .claude/
│ └── skills/ # the four Inngest skills (installed in the Quick Win)
│ ├── inngest-setup/SKILL.md
│ ├── inngest-events/SKILL.md
│ ├── inngest-steps/SKILL.md
│ └── inngest-durable-functions/SKILL.md
├── db.py # Neon Postgres access: pooled asyncpg, load_customers, record (closed-vocabulary audit) (D0)
├── worker.py # the worker: SandboxAgent + 2 tools (D0)
├── inngest_app.py # the nervous system: Inngest functions + FastAPI host (D1-D5)
├── .env # OPENAI_API_KEY, DATABASE_URL, INNGEST_DEV=1
└── AGENTS.md # the base's rules file (read on open)
یہ filenames ایک معقول layout ہیں، تقاضا نہیں؛ آپ کا agent اس کے بجائے agent.py اور main.py پر اتر سکتا ہے، اور وہ ٹھیک ہے۔ جو اہم ہے وہ حد ہے، نام نہیں: worker کوڈ کبھی inngest import نہیں کرتا، اور بالکل ایک فائل اوپر nervous system wire کرتی ہے۔ اُس layout کے ساتھ، گاہک اور audit trail آپ کے Neon database میں رہتے ہیں (Quick Win میں فراہم، D0 میں seeded)، نہ کہ مقامی فائلوں میں؛ worker فائلیں D0 کے بعد کبھی نہیں بدلتیں، اور ہر nervous-system layer (D1 سے D5 تک) اُس ایک Inngest فائل کو edit کرتی ہے۔
تشخیصی table، علامت → بنیادی وجہ → تصور
| علامت | پہلا مشتبہ | دوبارہ پڑھنے کا تصور |
|---|---|---|
| متوقع event آنے پر function کبھی fire نہیں ہوتا | event نام میں typo، namespace کا عدم مطابقت | C3 (webhooks)، C5 (fan-out) |
| وہی منطقی event کے لیے function دو بار fire ہوتا ہے | غائب idempotency key | C4 (idempotency) |
| deploy کے بعد function نے "کام کھویا" | کام کرتا ہوا کوڈ step.run سے باہر | C7 (memoization) |
| cron schedule ایک deploy کے دوران fire نہیں ہوا | صرف مقامی dev server، production Inngest infra پر چلتا ہے | C2 (cron) |
| ایک refund کے لیے گاہک سے دو بار charge ہوا | Stripe call step.run سے باہر، یا step نام منفرد نہیں | C6 (step.run)، C7 (memoization) |
| 9am peak کے دوران OpenAI rate-limit errors | غائب throttle | C11 (concurrency + throttle) |
| ایک گاہک کے bursts دوسرے گاہکوں کو بھوکا رکھتے ہیں | غائب per-key concurrency | C12 (priority + fairness) |
| function ہمیشہ کے لیے معطل، کبھی resume نہ ہوا | wait_for_event میں event نام بھیجے جانے والے event سے match نہیں کرتا | C8 (wait_for_event)، C15 (HITL) |
| HITL timeout ہفتے کے آخر میں خاموشی سے fire ہوا | غائب timeout handler جو audit میں لکھے | D5 (durable refund gate)، C15 (HITL) |
| کل کے ناکام runs dashboard سے غائب ہو گئے | runs اُس وقت تک برقرار رہتے ہیں جب تک manual replay یا retention window کے بعد | C14 (replay) |
| Replay نے گاہکوں سے دوبارہ charge کیا | Replay ایک تازہ run ہے جو ہر step دوبارہ execute کرتا ہے؛ charge میں کوئی idempotency key نہ تھی | C4 (idempotency)، C14 (replay ایک تازہ run ہے) |
| Function trace OpenAI prompt نہیں دکھاتا | step trace function inputs/outputs دکھاتا ہے مگر کوئی LLM-specific prompt/token telemetry نہیں | C10 (Python step.run استعمال کرتا ہے؛ LLM-specific telemetry کو آپ کی اپنی OpenAI client tracing چاہیے؛ step.ai.wrap کے prompt-level traces TypeScript-only ہیں) |
ضمیمہ: اختیاری نسب اور ایک Inngest cheat sheet
آپ کو حصہ 4 کرنے کے لیے Digital FTE کورس کی ضرورت نہیں: D0 worker کو شروع سے بناتا ہے۔ سیاق کے لیے دو مختصر نوٹ۔
A.1: اگر آپ Digital FTE کورس سے آ رہے ہیں
From Agent to Digital FTE کورس ایک امیر تر customer-support worker بناتا ہے: portable Skills، ایک Postgres system of record، اور ایک custom MCP server۔ اگر آپ نے یہ کیا، تو آپ کے پاس پہلے ہی disk پر ایک SandboxAgent worker بیٹھا ہے، اور آپ D0 کا کم سے کم floor چھوڑ سکتے ہیں: nervous system (D1 سے آگے) کو اپنے worker کی طرف اشارہ کریں۔ wrapping ایک جیسی ہے۔ ایک بونس: جو durable refund gate آپ D5 میں بناتے ہیں (step.wait_for_event) وہ اُس کورس کے اختیاری Decision 10 کے ہاتھ سے بنے run-state table کی جگہ لیتی ہے، تو آپ اسے delete کر سکتے ہیں۔ اگر آپ نے وہ کورس نہیں کیا، تو اس سب کو نظر انداز کریں؛ D0 آپ کو جو کچھ چاہیے سب دیتا ہے۔
A.2: Inngest-مخصوص لازمی چیزیں جو یہ کورس استعمال کرتا ہے
اگر نیچے کچھ بھی ناآشنا لگے، تو حصہ 4 میں غوطہ لگانے سے پہلے متعلقہ doc صفحہ skim کریں۔
- Inngest client instantiation. فی Python project ایک واحد
inngest.Inngest(app_id=...)instance، ایک module سے export اور جہاں بھی آپ functions decorate کریں import۔ Python quick start۔ - Function decoration.
@inngest_client.create_function(fn_id=..., trigger=...)۔ trigger ایکTriggerEvent،TriggerCron، یا multi-trigger functions کے لیے دونوں کی فہرست ہو سکتا ہے۔ ctx.step.run،ctx.step.sleep،ctx.step.wait_for_event،ctx.step.ai.infer. چار step primitives جو Python میں آپ جو لکھیں گے اس کا 90% بناتے ہیں۔ (TypeScript کے پاس ایک پانچواں ہے،step.ai.wrap، LLM-specific tracing کے لیے؛ Python projects AI calls کے لیےstep.runاستعمال کرتے ہیں۔)inngest_client.send(events=[...]). اپنے کوڈ میں کہیں سے بھی events emit کریں (functions کے اندر، agent tools کے اندر، CLI scripts سے)۔ idempotency کے لیے ایکid=استعمال کریں۔- Dev server startup.
npx inngest-cli@latest dev۔:8288پر چلتا ہے۔ dashboardhttp://127.0.0.1:8288پر۔ MCPhttp://127.0.0.1:8288/mcpپر۔ اگر:8288لیا گیا ہو تو یہ8289+استعمال کرتا ہے؛ پھر host پرINNGEST_BASE_URL=http://127.0.0.1:<port>سیٹ کریں تاکہ وہ پیروی کرے، نہ کہ صرف MCP URL۔
A.3: وہ دو تبدیلیاں جو واقعی مشکل ہیں
اس کورس کے بارے میں سب سے مشکل چیز Inngest کا syntax نہیں۔ یہ request سے event کی ذہنی تبدیلی (تصور 1) اور in-process execution سے durable execution (تصور 6) ہے۔ ایک بار یہ دونوں بیٹھ جائیں تو syntax مشینی ہے۔ اگر کوئی اور چیز اپنی توقع سے زیادہ مشکل لگے، تو پہلے تصور 1 اور 6 دوبارہ پڑھیں۔
Flashcards مطالعہ معاون
علمی جانچ
ابھی جن خیالات سے آپ گزرے ان پر ایک فوری gated خود-جانچ۔