[{"data":1,"prerenderedAt":2118},["ShallowReactive",2],{"navigation":3,"page:\u002Flangchain":45},{"title":4,"links":5},"API reference",[6,9,12,15,18,21,24,27,30,33,36,39,42],{"title":7,"href":8},"Accounts","\u002Freference\u002Faccounts",{"title":10,"href":11},"Connect","\u002Freference\u002Fconnect",{"title":13,"href":14},"Calendars","\u002Freference\u002Fcalendars",{"title":16,"href":17},"Events","\u002Freference\u002Fevents",{"title":19,"href":20},"Availability","\u002Freference\u002Favailability",{"title":22,"href":23},"Meetings","\u002Freference\u002Fmeetings",{"title":25,"href":26},"Invitations","\u002Freference\u002Finvitations",{"title":28,"href":29},"Webhooks","\u002Freference\u002Fwebhooks",{"title":31,"href":32},"Metadata","\u002Freference\u002Fmetadata",{"title":34,"href":35},"Keys","\u002Freference\u002Fkeys",{"title":37,"href":38},"Sandbox","\u002Freference\u002Fsandbox",{"title":40,"href":41},"Usage","\u002Freference\u002Fusage",{"title":43,"href":44},"Logs","\u002Freference\u002Flogs",{"kind":46,"title":47,"description":48,"sections":49,"doc":65},"guide","LangChain","Add a calendar assistant to your app with a LangChain agent and Pyas. It reads each user's Google and Outlook calendars, and pauses for their approval before it books, moves or deletes anything.",[50,53,56,59,62],{"id":51,"title":52},"install","Install",{"id":54,"title":55},"the-agent","The agent",{"id":57,"title":58},"your-routes","Your routes",{"id":60,"title":61},"show-the-change","Show the change",{"id":63,"title":64},"try-it","Try it",{"id":66,"title":47,"body":67,"description":48,"extension":2110,"hero":2111,"meta":2112,"navigation":260,"path":2113,"rawbody":2114,"sections":49,"seo":2115,"stem":73,"v1":2116,"__hash__":2117},"docs\u002Flangchain.md",{"type":68,"value":69,"toc":2103},"minimark",[70,74,81,99,102,139,142,175,190,192,1052,1059,1073,1076,1078,1081,1746,1760,1762,1769,2022,2033,2055,2062,2070,2072,2092,2099],[71,72,47],"h1",{"id":73},"langchain",[75,76,77],"lead",{},[78,79,80],"p",{},"This guide adds a calendar assistant to your server with a LangChain agent. The agent uses Pyas's tools for the signed-in user only. When it wants to change a calendar, it pauses until the person approves.",[78,82,83,84,88,89,92,93,98],{},"The code was tested with LangChain 1.6, LangGraph 1.7, and OpenAI's ",[85,86,87],"code",{},"gpt-5.5"," and Anthropic's ",[85,90,91],{},"claude-sonnet-5-5"," as the model. See ",[94,95,97],"a",{"href":96},"\u002Fai-in-your-app","AI in your app"," for how the tools work.",[100,101,52],"h2",{"id":51},[103,104,109],"pre",{"className":105,"code":106,"language":107,"meta":108,"style":108},"language-bash shiki shiki-themes css-variables","npm install @pyas\u002Fai-tools langchain @langchain\u002Fcore @langchain\u002Flanggraph @langchain\u002Fopenai\n","bash","",[85,110,111],{"__ignoreMap":108},[112,113,116,120,124,127,130,133,136],"span",{"class":114,"line":115},"line",1,[112,117,119],{"class":118},"s1IGz","npm",[112,121,123],{"class":122},"sTB4W"," install",[112,125,126],{"class":122}," @pyas\u002Fai-tools",[112,128,129],{"class":122}," langchain",[112,131,132],{"class":122}," @langchain\u002Fcore",[112,134,135],{"class":122}," @langchain\u002Flanggraph",[112,137,138],{"class":122}," @langchain\u002Fopenai\n",[78,140,141],{},"Set these on your server:",[103,143,145],{"className":105,"code":144,"language":107,"meta":108,"style":108},"PYAS_API_KEY=pyas_sk_test_...   # a test key from API keys in the Pyas dashboard\nOPENAI_API_KEY=sk-...\n",[85,146,147,164],{"__ignoreMap":108},[112,148,149,153,157,160],{"class":114,"line":115},[112,150,152],{"class":151},"sqSWz","PYAS_API_KEY",[112,154,156],{"class":155},"sag7P","=",[112,158,159],{"class":122},"pyas_sk_test_...",[112,161,163],{"class":162},"ssgCD","   # a test key from API keys in the Pyas dashboard\n",[112,165,167,170,172],{"class":114,"line":166},2,[112,168,169],{"class":151},"OPENAI_API_KEY",[112,171,156],{"class":155},[112,173,174],{"class":122},"sk-...\n",[78,176,177,178,181,182,185,186,189],{},"For Claude, install ",[85,179,180],{},"@langchain\u002Fanthropic",", set ",[85,183,184],{},"ANTHROPIC_API_KEY",", and use ",[85,187,188],{},"anthropic:claude-sonnet-5-5"," as the model.",[100,191,55],{"id":54},[103,193,198],{"className":194,"code":195,"filename":196,"language":197,"meta":108,"style":108},"language-ts shiki shiki-themes css-variables","import { createAgent } from 'langchain'\nimport { Command, MemorySaver } from '@langchain\u002Flanggraph'\nimport { pyasToolkit, type PyasApprovalRequest } from '@pyas\u002Fai-tools\u002Flangchain'\n\n\u002F\u002F Keeps each conversation between requests. In production, use a database checkpointer,\n\u002F\u002F such as PostgresSaver from @langchain\u002Flanggraph-checkpoint-postgres.\nconst checkpointer = new MemorySaver()\n\n\u002F** What to show the person: the assistant's reply, or the changes waiting for their approval. *\u002F\nexport type Turn =\n    | { reply: string }\n    | { changes: Array\u003C{ id: string } & PyasApprovalRequest> }\n\nfunction agentFor(userId: string) {\n    \u002F\u002F The same for the whole conversation. Today's date lets the model work out \"tomorrow\".\n    const today = new Date().toISOString().slice(0, 10)\n    return createAgent({\n        model: 'openai:gpt-5.5',\n        tools: pyasToolkit({ subjectId: userId }),\n        systemPrompt: `You help the user manage their calendar. Today is ${today}.`,\n        checkpointer,\n    })\n}\n\nfunction turn(result: { messages: Array\u003C{ text: string }>; __interrupt__?: Array\u003C{ id?: string; value?: unknown }> }): Turn {\n    const waiting = result.__interrupt__ ?? []\n    if (waiting.length) return { changes: waiting.map((change) => ({ id: change.id!, ...(change.value as PyasApprovalRequest) })) }\n    return { reply: result.messages.at(-1)?.text ?? '' }\n}\n\n\u002F** The person sends a message. conversationId is yours: one per conversation. *\u002F\nexport async function ask(conversationId: string, userId: string, text: string): Promise\u003CTurn> {\n    const config = { configurable: { thread_id: conversationId } }\n    return turn(await agentFor(userId).invoke({ messages: [{ role: 'user', content: text }] }, config))\n}\n\n\u002F** The person answers the changes: the ids they approved. Every other change is declined. *\u002F\nexport async function answer(conversationId: string, userId: string, changeIds: string[], approved: string[]): Promise\u003CTurn> {\n    const config = { configurable: { thread_id: conversationId } }\n    const resume = Object.fromEntries(changeIds.map((id) => [id, { approved: approved.includes(id) }]))\n    return turn(await agentFor(userId).invoke(new Command({ resume }), config))\n}\n","assistant.ts","ts",[85,199,200,215,234,255,262,268,274,296,301,307,321,339,371,376,395,401,442,454,468,489,514,522,528,534,539,598,620,694,731,736,741,747,803,826,874,879,884,890,950,969,1015,1047],{"__ignoreMap":108},[112,201,202,205,208,211],{"class":114,"line":115},[112,203,204],{"class":155},"import",[112,206,207],{"class":151}," { createAgent } ",[112,209,210],{"class":155},"from",[112,212,214],{"class":213},"sk1Au"," 'langchain'\n",[112,216,217,219,222,226,229,231],{"class":114,"line":166},[112,218,204],{"class":155},[112,220,221],{"class":151}," { Command",[112,223,225],{"class":224},"sEEt9",",",[112,227,228],{"class":151}," MemorySaver } ",[112,230,210],{"class":155},[112,232,233],{"class":213}," '@langchain\u002Flanggraph'\n",[112,235,237,239,242,244,247,250,252],{"class":114,"line":236},3,[112,238,204],{"class":155},[112,240,241],{"class":151}," { pyasToolkit",[112,243,225],{"class":224},[112,245,246],{"class":155}," type",[112,248,249],{"class":151}," PyasApprovalRequest } ",[112,251,210],{"class":155},[112,253,254],{"class":213}," '@pyas\u002Fai-tools\u002Flangchain'\n",[112,256,258],{"class":114,"line":257},4,[112,259,261],{"emptyLinePlaceholder":260},true,"\n",[112,263,265],{"class":114,"line":264},5,[112,266,267],{"class":162},"\u002F\u002F Keeps each conversation between requests. In production, use a database checkpointer,\n",[112,269,271],{"class":114,"line":270},6,[112,272,273],{"class":162},"\u002F\u002F such as PostgresSaver from @langchain\u002Flanggraph-checkpoint-postgres.\n",[112,275,277,280,284,287,290,293],{"class":114,"line":276},7,[112,278,279],{"class":155},"const",[112,281,283],{"class":282},"sXULy"," checkpointer",[112,285,286],{"class":155}," =",[112,288,289],{"class":155}," new",[112,291,292],{"class":118}," MemorySaver",[112,294,295],{"class":151},"()\n",[112,297,299],{"class":114,"line":298},8,[112,300,261],{"emptyLinePlaceholder":260},[112,302,304],{"class":114,"line":303},9,[112,305,306],{"class":162},"\u002F** What to show the person: the assistant's reply, or the changes waiting for their approval. *\u002F\n",[112,308,310,313,315,318],{"class":114,"line":309},10,[112,311,312],{"class":155},"export",[112,314,246],{"class":155},[112,316,317],{"class":118}," Turn",[112,319,320],{"class":155}," =\n",[112,322,324,327,330,333,336],{"class":114,"line":323},11,[112,325,326],{"class":155},"    |",[112,328,329],{"class":151}," { reply",[112,331,332],{"class":155},":",[112,334,335],{"class":282}," string",[112,337,338],{"class":151}," }\n",[112,340,342,344,347,349,352,355,357,359,362,365,368],{"class":114,"line":341},12,[112,343,326],{"class":155},[112,345,346],{"class":151}," { changes",[112,348,332],{"class":155},[112,350,351],{"class":118}," Array",[112,353,354],{"class":151},"\u003C{ id",[112,356,332],{"class":155},[112,358,335],{"class":282},[112,360,361],{"class":151}," } ",[112,363,364],{"class":155},"&",[112,366,367],{"class":118}," PyasApprovalRequest",[112,369,370],{"class":151},"> }\n",[112,372,374],{"class":114,"line":373},13,[112,375,261],{"emptyLinePlaceholder":260},[112,377,379,382,385,388,390,392],{"class":114,"line":378},14,[112,380,381],{"class":155},"function",[112,383,384],{"class":118}," agentFor",[112,386,387],{"class":151},"(userId",[112,389,332],{"class":155},[112,391,335],{"class":282},[112,393,394],{"class":151},") {\n",[112,396,398],{"class":114,"line":397},15,[112,399,400],{"class":162},"    \u002F\u002F The same for the whole conversation. Today's date lets the model work out \"tomorrow\".\n",[112,402,404,407,410,412,414,417,420,423,425,428,431,434,436,439],{"class":114,"line":403},16,[112,405,406],{"class":155},"    const",[112,408,409],{"class":282}," today",[112,411,286],{"class":155},[112,413,289],{"class":155},[112,415,416],{"class":118}," Date",[112,418,419],{"class":151},"()",[112,421,422],{"class":118},".toISOString",[112,424,419],{"class":151},[112,426,427],{"class":118},".slice",[112,429,430],{"class":151},"(",[112,432,433],{"class":282},"0",[112,435,225],{"class":224},[112,437,438],{"class":282}," 10",[112,440,441],{"class":151},")\n",[112,443,445,448,451],{"class":114,"line":444},17,[112,446,447],{"class":155},"    return",[112,449,450],{"class":118}," createAgent",[112,452,453],{"class":151},"({\n",[112,455,457,460,462,465],{"class":114,"line":456},18,[112,458,459],{"class":151},"        model",[112,461,332],{"class":155},[112,463,464],{"class":213}," 'openai:gpt-5.5'",[112,466,467],{"class":224},",\n",[112,469,471,474,476,479,482,484,487],{"class":114,"line":470},19,[112,472,473],{"class":151},"        tools",[112,475,332],{"class":155},[112,477,478],{"class":118}," pyasToolkit",[112,480,481],{"class":151},"({ subjectId",[112,483,332],{"class":155},[112,485,486],{"class":151}," userId })",[112,488,467],{"class":224},[112,490,492,495,497,500,503,506,509,512],{"class":114,"line":491},20,[112,493,494],{"class":151},"        systemPrompt",[112,496,332],{"class":155},[112,498,499],{"class":213}," `You help the user manage their calendar. Today is ",[112,501,502],{"class":155},"${",[112,504,505],{"class":151},"today",[112,507,508],{"class":155},"}",[112,510,511],{"class":213},".`",[112,513,467],{"class":224},[112,515,517,520],{"class":114,"line":516},21,[112,518,519],{"class":151},"        checkpointer",[112,521,467],{"class":224},[112,523,525],{"class":114,"line":524},22,[112,526,527],{"class":151},"    })\n",[112,529,531],{"class":114,"line":530},23,[112,532,533],{"class":151},"}\n",[112,535,537],{"class":114,"line":536},24,[112,538,261],{"emptyLinePlaceholder":260},[112,540,542,544,547,550,552,555,557,559,562,564,566,569,572,574,576,578,580,583,585,588,591,593,595],{"class":114,"line":541},25,[112,543,381],{"class":155},[112,545,546],{"class":118}," turn",[112,548,549],{"class":151},"(result",[112,551,332],{"class":155},[112,553,554],{"class":151}," { messages",[112,556,332],{"class":155},[112,558,351],{"class":118},[112,560,561],{"class":151},"\u003C{ text",[112,563,332],{"class":155},[112,565,335],{"class":282},[112,567,568],{"class":151}," }>; __interrupt__",[112,570,571],{"class":155},"?:",[112,573,351],{"class":118},[112,575,354],{"class":151},[112,577,571],{"class":155},[112,579,335],{"class":282},[112,581,582],{"class":151},"; value",[112,584,571],{"class":155},[112,586,587],{"class":282}," unknown",[112,589,590],{"class":151}," }> })",[112,592,332],{"class":155},[112,594,317],{"class":118},[112,596,597],{"class":151}," {\n",[112,599,601,603,606,608,611,614,617],{"class":114,"line":600},26,[112,602,406],{"class":155},[112,604,605],{"class":282}," waiting",[112,607,286],{"class":155},[112,609,610],{"class":282}," result",[112,612,613],{"class":151},".__interrupt__ ",[112,615,616],{"class":155},"??",[112,618,619],{"class":151}," []\n",[112,621,623,626,629,632,635,638,641,644,646,648,650,653,656,659,662,664,667,670,673,675,678,680,683,686,689,691],{"class":114,"line":622},27,[112,624,625],{"class":155},"    if",[112,627,628],{"class":151}," (",[112,630,631],{"class":282},"waiting",[112,633,634],{"class":151},".",[112,636,637],{"class":282},"length",[112,639,640],{"class":151},") ",[112,642,643],{"class":155},"return",[112,645,346],{"class":151},[112,647,332],{"class":155},[112,649,605],{"class":282},[112,651,652],{"class":118},".map",[112,654,655],{"class":151},"((change) ",[112,657,658],{"class":155},"=>",[112,660,661],{"class":151}," ({ id",[112,663,332],{"class":155},[112,665,666],{"class":282}," change",[112,668,669],{"class":151},".id",[112,671,672],{"class":155},"!",[112,674,225],{"class":224},[112,676,677],{"class":155}," ...",[112,679,430],{"class":151},[112,681,682],{"class":282},"change",[112,684,685],{"class":151},".value ",[112,687,688],{"class":155},"as",[112,690,367],{"class":118},[112,692,693],{"class":151},") })) }\n",[112,695,697,699,701,703,705,707,710,713,715,718,721,724,726,729],{"class":114,"line":696},28,[112,698,447],{"class":155},[112,700,329],{"class":151},[112,702,332],{"class":155},[112,704,610],{"class":282},[112,706,634],{"class":118},[112,708,709],{"class":282},"messages",[112,711,712],{"class":118},".at",[112,714,430],{"class":151},[112,716,717],{"class":155},"-",[112,719,720],{"class":282},"1",[112,722,723],{"class":151},")?.text ",[112,725,616],{"class":155},[112,727,728],{"class":213}," ''",[112,730,338],{"class":151},[112,732,734],{"class":114,"line":733},29,[112,735,533],{"class":151},[112,737,739],{"class":114,"line":738},30,[112,740,261],{"emptyLinePlaceholder":260},[112,742,744],{"class":114,"line":743},31,[112,745,746],{"class":162},"\u002F** The person sends a message. conversationId is yours: one per conversation. *\u002F\n",[112,748,750,752,755,758,761,764,766,768,770,773,775,777,779,782,784,786,789,791,794,797,800],{"class":114,"line":749},32,[112,751,312],{"class":155},[112,753,754],{"class":155}," async",[112,756,757],{"class":155}," function",[112,759,760],{"class":118}," ask",[112,762,763],{"class":151},"(conversationId",[112,765,332],{"class":155},[112,767,335],{"class":282},[112,769,225],{"class":224},[112,771,772],{"class":151}," userId",[112,774,332],{"class":155},[112,776,335],{"class":282},[112,778,225],{"class":224},[112,780,781],{"class":151}," text",[112,783,332],{"class":155},[112,785,335],{"class":282},[112,787,788],{"class":151},")",[112,790,332],{"class":155},[112,792,793],{"class":118}," Promise",[112,795,796],{"class":151},"\u003C",[112,798,799],{"class":118},"Turn",[112,801,802],{"class":151},"> {\n",[112,804,806,808,811,813,816,818,821,823],{"class":114,"line":805},33,[112,807,406],{"class":155},[112,809,810],{"class":282}," config",[112,812,286],{"class":155},[112,814,815],{"class":151}," { configurable",[112,817,332],{"class":155},[112,819,820],{"class":151}," { thread_id",[112,822,332],{"class":155},[112,824,825],{"class":151}," conversationId } }\n",[112,827,829,831,833,835,838,840,843,846,849,851,854,856,859,861,864,866,869,871],{"class":114,"line":828},34,[112,830,447],{"class":155},[112,832,546],{"class":118},[112,834,430],{"class":151},[112,836,837],{"class":155},"await",[112,839,384],{"class":118},[112,841,842],{"class":151},"(userId)",[112,844,845],{"class":118},".invoke",[112,847,848],{"class":151},"({ messages",[112,850,332],{"class":155},[112,852,853],{"class":151}," [{ role",[112,855,332],{"class":155},[112,857,858],{"class":213}," 'user'",[112,860,225],{"class":224},[112,862,863],{"class":151}," content",[112,865,332],{"class":155},[112,867,868],{"class":151}," text }] }",[112,870,225],{"class":224},[112,872,873],{"class":151}," config))\n",[112,875,877],{"class":114,"line":876},35,[112,878,533],{"class":151},[112,880,882],{"class":114,"line":881},36,[112,883,261],{"emptyLinePlaceholder":260},[112,885,887],{"class":114,"line":886},37,[112,888,889],{"class":162},"\u002F** The person answers the changes: the ids they approved. Every other change is declined. *\u002F\n",[112,891,893,895,897,899,902,904,906,908,910,912,914,916,918,921,923,925,928,930,933,935,937,940,942,944,946,948],{"class":114,"line":892},38,[112,894,312],{"class":155},[112,896,754],{"class":155},[112,898,757],{"class":155},[112,900,901],{"class":118}," answer",[112,903,763],{"class":151},[112,905,332],{"class":155},[112,907,335],{"class":282},[112,909,225],{"class":224},[112,911,772],{"class":151},[112,913,332],{"class":155},[112,915,335],{"class":282},[112,917,225],{"class":224},[112,919,920],{"class":151}," changeIds",[112,922,332],{"class":155},[112,924,335],{"class":282},[112,926,927],{"class":151},"[]",[112,929,225],{"class":224},[112,931,932],{"class":151}," approved",[112,934,332],{"class":155},[112,936,335],{"class":282},[112,938,939],{"class":151},"[])",[112,941,332],{"class":155},[112,943,793],{"class":118},[112,945,796],{"class":151},[112,947,799],{"class":118},[112,949,802],{"class":151},[112,951,953,955,957,959,961,963,965,967],{"class":114,"line":952},39,[112,954,406],{"class":155},[112,956,810],{"class":282},[112,958,286],{"class":155},[112,960,815],{"class":151},[112,962,332],{"class":155},[112,964,820],{"class":151},[112,966,332],{"class":155},[112,968,825],{"class":151},[112,970,972,974,977,979,982,985,987,990,992,995,997,1000,1002,1005,1007,1009,1012],{"class":114,"line":971},40,[112,973,406],{"class":155},[112,975,976],{"class":282}," resume",[112,978,286],{"class":155},[112,980,981],{"class":282}," Object",[112,983,984],{"class":118},".fromEntries",[112,986,430],{"class":151},[112,988,989],{"class":282},"changeIds",[112,991,652],{"class":118},[112,993,994],{"class":151},"((id) ",[112,996,658],{"class":155},[112,998,999],{"class":151}," [id",[112,1001,225],{"class":224},[112,1003,1004],{"class":151}," { approved",[112,1006,332],{"class":155},[112,1008,932],{"class":282},[112,1010,1011],{"class":118},".includes",[112,1013,1014],{"class":151},"(id) }]))\n",[112,1016,1018,1020,1022,1024,1026,1028,1030,1032,1034,1037,1040,1043,1045],{"class":114,"line":1017},41,[112,1019,447],{"class":155},[112,1021,546],{"class":118},[112,1023,430],{"class":151},[112,1025,837],{"class":155},[112,1027,384],{"class":118},[112,1029,842],{"class":151},[112,1031,845],{"class":118},[112,1033,430],{"class":151},[112,1035,1036],{"class":155},"new",[112,1038,1039],{"class":118}," Command",[112,1041,1042],{"class":151},"({ resume })",[112,1044,225],{"class":224},[112,1046,873],{"class":151},[112,1048,1050],{"class":114,"line":1049},42,[112,1051,533],{"class":151},[78,1053,1054,1055,1058],{},"A tool that changes a calendar pauses the agent with LangGraph's ",[85,1056,1057],{},"interrupt()",". The agent needs a checkpointer to pause: it saves the conversation, and the agent carries on from the same point once the person approves or declines. Outside an agent, such a tool fails rather than run without approval.",[78,1060,1061,1064,1065,1068,1069,1072],{},[85,1062,1063],{},"answer"," resumes the agent with ",[85,1066,1067],{},"{ approved: true }"," or ",[85,1070,1071],{},"{ approved: false }"," for each change, by the change's ID. A declined change is never sent to Pyas, and the model is told.",[78,1074,1075],{},"Keep the system prompt the same for the whole conversation. Some models, such as Claude, reject a conversation whose system prompt changes partway, so put today's date in it, not the time.",[100,1077,58],{"id":57},[78,1079,1080],{},"Your server keeps each person's conversation ID and the changes waiting for them. With Express:",[103,1082,1085],{"className":194,"code":1083,"filename":1084,"language":197,"meta":108,"style":108},"import express from 'express'\nimport { randomUUID } from 'node:crypto'\nimport { answer, ask } from '.\u002Fassistant.js'\n\n\u002F** The signed-in person's ID in your app, from your sign-in (here, the sandbox's Avery Brooks). *\u002F\nfunction currentUserId(_req: express.Request): string | null {\n    return 'sandbox_user_1'\n}\n\n\u002F\u002F Each person's conversation and the changes waiting for them. In production, keep these in your database.\nconst conversations = new Map\u003Cstring, { id: string; waiting: string[] }>()\n\nconst app = express()\napp.use(express.json())\n\napp.post('\u002Fchat', async (req, res) => {\n    const userId = currentUserId(req)\n    if (!userId) return res.status(401).json({ error: 'Sign in to use your calendar.' })\n    const conversation = conversations.get(userId) ?? { id: randomUUID(), waiting: [] }\n    conversations.set(userId, conversation)\n    const turn = await ask(conversation.id, userId, req.body.text)\n    conversation.waiting = 'changes' in turn ? turn.changes.map((change) => change.id) : []\n    res.json(turn)\n})\n\n\u002F\u002F The person's answer: { \"approved\": [\"...\"] }. Changes left out are declined.\napp.post('\u002Fchat\u002Fanswer', async (req, res) => {\n    const userId = currentUserId(req)\n    if (!userId) return res.status(401).json({ error: 'Sign in to use your calendar.' })\n    const conversation = conversations.get(userId)\n    if (!conversation?.waiting.length) return res.status(409).json({ error: 'No change is waiting for an answer.' })\n    const turn = await answer(conversation.id, userId, conversation.waiting, req.body.approved ?? [])\n    conversation.waiting = 'changes' in turn ? turn.changes.map((change) => change.id) : []\n    res.json(turn)\n})\n\napp.listen(3457)\n","server.ts",[85,1086,1087,1099,1111,1128,1132,1137,1171,1178,1182,1186,1191,1229,1233,1246,1265,1269,1297,1310,1349,1386,1401,1438,1482,1492,1497,1501,1506,1531,1543,1575,1590,1635,1680,1716,1724,1728,1732],{"__ignoreMap":108},[112,1088,1089,1091,1094,1096],{"class":114,"line":115},[112,1090,204],{"class":155},[112,1092,1093],{"class":151}," express ",[112,1095,210],{"class":155},[112,1097,1098],{"class":213}," 'express'\n",[112,1100,1101,1103,1106,1108],{"class":114,"line":166},[112,1102,204],{"class":155},[112,1104,1105],{"class":151}," { randomUUID } ",[112,1107,210],{"class":155},[112,1109,1110],{"class":213}," 'node:crypto'\n",[112,1112,1113,1115,1118,1120,1123,1125],{"class":114,"line":236},[112,1114,204],{"class":155},[112,1116,1117],{"class":151}," { answer",[112,1119,225],{"class":224},[112,1121,1122],{"class":151}," ask } ",[112,1124,210],{"class":155},[112,1126,1127],{"class":213}," '.\u002Fassistant.js'\n",[112,1129,1130],{"class":114,"line":257},[112,1131,261],{"emptyLinePlaceholder":260},[112,1133,1134],{"class":114,"line":264},[112,1135,1136],{"class":162},"\u002F** The signed-in person's ID in your app, from your sign-in (here, the sandbox's Avery Brooks). *\u002F\n",[112,1138,1139,1141,1144,1147,1149,1152,1154,1157,1159,1161,1163,1166,1169],{"class":114,"line":270},[112,1140,381],{"class":155},[112,1142,1143],{"class":118}," currentUserId",[112,1145,1146],{"class":151},"(_req",[112,1148,332],{"class":155},[112,1150,1151],{"class":118}," express",[112,1153,634],{"class":151},[112,1155,1156],{"class":118},"Request",[112,1158,788],{"class":151},[112,1160,332],{"class":155},[112,1162,335],{"class":282},[112,1164,1165],{"class":155}," |",[112,1167,1168],{"class":282}," null",[112,1170,597],{"class":151},[112,1172,1173,1175],{"class":114,"line":276},[112,1174,447],{"class":155},[112,1176,1177],{"class":213}," 'sandbox_user_1'\n",[112,1179,1180],{"class":114,"line":298},[112,1181,533],{"class":151},[112,1183,1184],{"class":114,"line":303},[112,1185,261],{"emptyLinePlaceholder":260},[112,1187,1188],{"class":114,"line":309},[112,1189,1190],{"class":162},"\u002F\u002F Each person's conversation and the changes waiting for them. In production, keep these in your database.\n",[112,1192,1193,1195,1198,1200,1202,1205,1207,1210,1212,1215,1217,1219,1222,1224,1226],{"class":114,"line":323},[112,1194,279],{"class":155},[112,1196,1197],{"class":282}," conversations",[112,1199,286],{"class":155},[112,1201,289],{"class":155},[112,1203,1204],{"class":118}," Map",[112,1206,796],{"class":151},[112,1208,1209],{"class":282},"string",[112,1211,225],{"class":224},[112,1213,1214],{"class":151}," { id",[112,1216,332],{"class":155},[112,1218,335],{"class":282},[112,1220,1221],{"class":151},"; waiting",[112,1223,332],{"class":155},[112,1225,335],{"class":282},[112,1227,1228],{"class":151},"[] }>()\n",[112,1230,1231],{"class":114,"line":341},[112,1232,261],{"emptyLinePlaceholder":260},[112,1234,1235,1237,1240,1242,1244],{"class":114,"line":373},[112,1236,279],{"class":155},[112,1238,1239],{"class":282}," app",[112,1241,286],{"class":155},[112,1243,1151],{"class":118},[112,1245,295],{"class":151},[112,1247,1248,1251,1254,1256,1259,1262],{"class":114,"line":378},[112,1249,1250],{"class":282},"app",[112,1252,1253],{"class":118},".use",[112,1255,430],{"class":151},[112,1257,1258],{"class":282},"express",[112,1260,1261],{"class":118},".json",[112,1263,1264],{"class":151},"())\n",[112,1266,1267],{"class":114,"line":397},[112,1268,261],{"emptyLinePlaceholder":260},[112,1270,1271,1273,1276,1278,1281,1283,1285,1288,1290,1293,1295],{"class":114,"line":403},[112,1272,1250],{"class":282},[112,1274,1275],{"class":118},".post",[112,1277,430],{"class":151},[112,1279,1280],{"class":213},"'\u002Fchat'",[112,1282,225],{"class":224},[112,1284,754],{"class":155},[112,1286,1287],{"class":151}," (req",[112,1289,225],{"class":224},[112,1291,1292],{"class":151}," res) ",[112,1294,658],{"class":155},[112,1296,597],{"class":151},[112,1298,1299,1301,1303,1305,1307],{"class":114,"line":444},[112,1300,406],{"class":155},[112,1302,772],{"class":282},[112,1304,286],{"class":155},[112,1306,1143],{"class":118},[112,1308,1309],{"class":151},"(req)\n",[112,1311,1312,1314,1316,1318,1321,1323,1326,1329,1331,1334,1336,1338,1341,1343,1346],{"class":114,"line":456},[112,1313,625],{"class":155},[112,1315,628],{"class":151},[112,1317,672],{"class":155},[112,1319,1320],{"class":151},"userId) ",[112,1322,643],{"class":155},[112,1324,1325],{"class":282}," res",[112,1327,1328],{"class":118},".status",[112,1330,430],{"class":151},[112,1332,1333],{"class":282},"401",[112,1335,788],{"class":151},[112,1337,1261],{"class":118},[112,1339,1340],{"class":151},"({ error",[112,1342,332],{"class":155},[112,1344,1345],{"class":213}," 'Sign in to use your calendar.'",[112,1347,1348],{"class":151}," })\n",[112,1350,1351,1353,1356,1358,1360,1363,1366,1368,1370,1372,1375,1377,1379,1381,1383],{"class":114,"line":470},[112,1352,406],{"class":155},[112,1354,1355],{"class":282}," conversation",[112,1357,286],{"class":155},[112,1359,1197],{"class":282},[112,1361,1362],{"class":118},".get",[112,1364,1365],{"class":151},"(userId) ",[112,1367,616],{"class":155},[112,1369,1214],{"class":151},[112,1371,332],{"class":155},[112,1373,1374],{"class":118}," randomUUID",[112,1376,419],{"class":151},[112,1378,225],{"class":224},[112,1380,605],{"class":151},[112,1382,332],{"class":155},[112,1384,1385],{"class":151}," [] }\n",[112,1387,1388,1391,1394,1396,1398],{"class":114,"line":491},[112,1389,1390],{"class":282},"    conversations",[112,1392,1393],{"class":118},".set",[112,1395,387],{"class":151},[112,1397,225],{"class":224},[112,1399,1400],{"class":151}," conversation)\n",[112,1402,1403,1405,1407,1409,1412,1414,1416,1419,1421,1423,1425,1427,1430,1432,1435],{"class":114,"line":516},[112,1404,406],{"class":155},[112,1406,546],{"class":282},[112,1408,286],{"class":155},[112,1410,1411],{"class":155}," await",[112,1413,760],{"class":118},[112,1415,430],{"class":151},[112,1417,1418],{"class":282},"conversation",[112,1420,669],{"class":151},[112,1422,225],{"class":224},[112,1424,772],{"class":151},[112,1426,225],{"class":224},[112,1428,1429],{"class":282}," req",[112,1431,634],{"class":151},[112,1433,1434],{"class":282},"body",[112,1436,1437],{"class":151},".text)\n",[112,1439,1440,1443,1446,1448,1451,1454,1457,1460,1462,1464,1467,1469,1471,1473,1475,1478,1480],{"class":114,"line":524},[112,1441,1442],{"class":282},"    conversation",[112,1444,1445],{"class":151},".waiting ",[112,1447,156],{"class":155},[112,1449,1450],{"class":213}," 'changes'",[112,1452,1453],{"class":155}," in",[112,1455,1456],{"class":151}," turn ",[112,1458,1459],{"class":155},"?",[112,1461,546],{"class":282},[112,1463,634],{"class":118},[112,1465,1466],{"class":282},"changes",[112,1468,652],{"class":118},[112,1470,655],{"class":151},[112,1472,658],{"class":155},[112,1474,666],{"class":282},[112,1476,1477],{"class":151},".id) ",[112,1479,332],{"class":155},[112,1481,619],{"class":151},[112,1483,1484,1487,1489],{"class":114,"line":530},[112,1485,1486],{"class":282},"    res",[112,1488,1261],{"class":118},[112,1490,1491],{"class":151},"(turn)\n",[112,1493,1494],{"class":114,"line":536},[112,1495,1496],{"class":151},"})\n",[112,1498,1499],{"class":114,"line":541},[112,1500,261],{"emptyLinePlaceholder":260},[112,1502,1503],{"class":114,"line":600},[112,1504,1505],{"class":162},"\u002F\u002F The person's answer: { \"approved\": [\"...\"] }. Changes left out are declined.\n",[112,1507,1508,1510,1512,1514,1517,1519,1521,1523,1525,1527,1529],{"class":114,"line":622},[112,1509,1250],{"class":282},[112,1511,1275],{"class":118},[112,1513,430],{"class":151},[112,1515,1516],{"class":213},"'\u002Fchat\u002Fanswer'",[112,1518,225],{"class":224},[112,1520,754],{"class":155},[112,1522,1287],{"class":151},[112,1524,225],{"class":224},[112,1526,1292],{"class":151},[112,1528,658],{"class":155},[112,1530,597],{"class":151},[112,1532,1533,1535,1537,1539,1541],{"class":114,"line":696},[112,1534,406],{"class":155},[112,1536,772],{"class":282},[112,1538,286],{"class":155},[112,1540,1143],{"class":118},[112,1542,1309],{"class":151},[112,1544,1545,1547,1549,1551,1553,1555,1557,1559,1561,1563,1565,1567,1569,1571,1573],{"class":114,"line":733},[112,1546,625],{"class":155},[112,1548,628],{"class":151},[112,1550,672],{"class":155},[112,1552,1320],{"class":151},[112,1554,643],{"class":155},[112,1556,1325],{"class":282},[112,1558,1328],{"class":118},[112,1560,430],{"class":151},[112,1562,1333],{"class":282},[112,1564,788],{"class":151},[112,1566,1261],{"class":118},[112,1568,1340],{"class":151},[112,1570,332],{"class":155},[112,1572,1345],{"class":213},[112,1574,1348],{"class":151},[112,1576,1577,1579,1581,1583,1585,1587],{"class":114,"line":738},[112,1578,406],{"class":155},[112,1580,1355],{"class":282},[112,1582,286],{"class":155},[112,1584,1197],{"class":282},[112,1586,1362],{"class":118},[112,1588,1589],{"class":151},"(userId)\n",[112,1591,1592,1594,1596,1598,1600,1603,1605,1607,1609,1611,1613,1615,1617,1619,1622,1624,1626,1628,1630,1633],{"class":114,"line":743},[112,1593,625],{"class":155},[112,1595,628],{"class":151},[112,1597,672],{"class":155},[112,1599,1418],{"class":282},[112,1601,1602],{"class":151},"?.",[112,1604,631],{"class":282},[112,1606,634],{"class":151},[112,1608,637],{"class":282},[112,1610,640],{"class":151},[112,1612,643],{"class":155},[112,1614,1325],{"class":282},[112,1616,1328],{"class":118},[112,1618,430],{"class":151},[112,1620,1621],{"class":282},"409",[112,1623,788],{"class":151},[112,1625,1261],{"class":118},[112,1627,1340],{"class":151},[112,1629,332],{"class":155},[112,1631,1632],{"class":213}," 'No change is waiting for an answer.'",[112,1634,1348],{"class":151},[112,1636,1637,1639,1641,1643,1645,1647,1649,1651,1653,1655,1657,1659,1661,1664,1666,1668,1670,1672,1675,1677],{"class":114,"line":749},[112,1638,406],{"class":155},[112,1640,546],{"class":282},[112,1642,286],{"class":155},[112,1644,1411],{"class":155},[112,1646,901],{"class":118},[112,1648,430],{"class":151},[112,1650,1418],{"class":282},[112,1652,669],{"class":151},[112,1654,225],{"class":224},[112,1656,772],{"class":151},[112,1658,225],{"class":224},[112,1660,1355],{"class":282},[112,1662,1663],{"class":151},".waiting",[112,1665,225],{"class":224},[112,1667,1429],{"class":282},[112,1669,634],{"class":151},[112,1671,1434],{"class":282},[112,1673,1674],{"class":151},".approved ",[112,1676,616],{"class":155},[112,1678,1679],{"class":151}," [])\n",[112,1681,1682,1684,1686,1688,1690,1692,1694,1696,1698,1700,1702,1704,1706,1708,1710,1712,1714],{"class":114,"line":805},[112,1683,1442],{"class":282},[112,1685,1445],{"class":151},[112,1687,156],{"class":155},[112,1689,1450],{"class":213},[112,1691,1453],{"class":155},[112,1693,1456],{"class":151},[112,1695,1459],{"class":155},[112,1697,546],{"class":282},[112,1699,634],{"class":118},[112,1701,1466],{"class":282},[112,1703,652],{"class":118},[112,1705,655],{"class":151},[112,1707,658],{"class":155},[112,1709,666],{"class":282},[112,1711,1477],{"class":151},[112,1713,332],{"class":155},[112,1715,619],{"class":151},[112,1717,1718,1720,1722],{"class":114,"line":828},[112,1719,1486],{"class":282},[112,1721,1261],{"class":118},[112,1723,1491],{"class":151},[112,1725,1726],{"class":114,"line":876},[112,1727,1496],{"class":151},[112,1729,1730],{"class":114,"line":881},[112,1731,261],{"emptyLinePlaceholder":260},[112,1733,1734,1736,1739,1741,1744],{"class":114,"line":886},[112,1735,1250],{"class":282},[112,1737,1738],{"class":118},".listen",[112,1740,430],{"class":151},[112,1742,1743],{"class":282},"3457",[112,1745,441],{"class":151},[78,1747,1748,1751,1752,1755,1756,1759],{},[85,1749,1750],{},"currentUserId"," stands for your sign-in. It returns the ID you saved as ",[85,1753,1754],{},"subjectId"," when the person connected their calendar. To try the guide in the sandbox, have it return ",[85,1757,1758],{},"'sandbox_user_1'",", the sandbox person Avery Brooks.",[100,1761,61],{"id":60},[78,1763,1764,1765,1768],{},"When the agent wants to change something, ",[85,1766,1767],{},"ask"," returns the changes instead of a reply:",[103,1770,1774],{"className":1771,"code":1772,"language":1773,"meta":108,"style":108},"language-json shiki shiki-themes css-variables","{\n  \"changes\": [\n    {\n      \"id\": \"c9d154673bb91cf774e8a964e2891f8c\",\n      \"type\": \"pyas_approval\",\n      \"tool\": \"delete_event\",\n      \"title\": \"Delete an event\",\n      \"input\": { \"accountId\": \"sbx_f7bb6446c0_1\", \"eventId\": \"sbc90a16505a04a760bc4a34a0\" },\n      \"event\": {\n        \"id\": \"sbc90a16505a04a760bc4a34a0\",\n        \"title\": \"Design Review\",\n        \"startsAt\": \"2026-11-10T20:00:00Z\",\n        \"endsAt\": \"2026-11-10T21:00:00Z\",\n        \"startsAtLocal\": \"2026-11-10T15:00:00-05:00\",\n        \"endsAtLocal\": \"2026-11-10T16:00:00-05:00\",\n        \"timezone\": \"America\u002FNew_York\",\n        \"isAllDay\": false,\n        \"location\": \"Room 4B\",\n        \"version\": \"\\\"1\\\"\"\n      }\n    }\n  ]\n}\n","json",[85,1775,1776,1781,1791,1796,1808,1820,1832,1844,1877,1886,1897,1909,1921,1933,1945,1957,1969,1981,1993,2003,2008,2013,2018],{"__ignoreMap":108},[112,1777,1778],{"class":114,"line":115},[112,1779,1780],{"class":151},"{\n",[112,1782,1783,1786,1788],{"class":114,"line":166},[112,1784,1785],{"class":155},"  \"changes\"",[112,1787,332],{"class":224},[112,1789,1790],{"class":151}," [\n",[112,1792,1793],{"class":114,"line":236},[112,1794,1795],{"class":151},"    {\n",[112,1797,1798,1801,1803,1806],{"class":114,"line":257},[112,1799,1800],{"class":155},"      \"id\"",[112,1802,332],{"class":224},[112,1804,1805],{"class":213}," \"c9d154673bb91cf774e8a964e2891f8c\"",[112,1807,467],{"class":224},[112,1809,1810,1813,1815,1818],{"class":114,"line":264},[112,1811,1812],{"class":155},"      \"type\"",[112,1814,332],{"class":224},[112,1816,1817],{"class":213}," \"pyas_approval\"",[112,1819,467],{"class":224},[112,1821,1822,1825,1827,1830],{"class":114,"line":270},[112,1823,1824],{"class":155},"      \"tool\"",[112,1826,332],{"class":224},[112,1828,1829],{"class":213}," \"delete_event\"",[112,1831,467],{"class":224},[112,1833,1834,1837,1839,1842],{"class":114,"line":276},[112,1835,1836],{"class":155},"      \"title\"",[112,1838,332],{"class":224},[112,1840,1841],{"class":213}," \"Delete an event\"",[112,1843,467],{"class":224},[112,1845,1846,1849,1851,1854,1857,1859,1862,1864,1867,1869,1872,1875],{"class":114,"line":298},[112,1847,1848],{"class":155},"      \"input\"",[112,1850,332],{"class":224},[112,1852,1853],{"class":151}," { ",[112,1855,1856],{"class":155},"\"accountId\"",[112,1858,332],{"class":224},[112,1860,1861],{"class":213}," \"sbx_f7bb6446c0_1\"",[112,1863,225],{"class":224},[112,1865,1866],{"class":155}," \"eventId\"",[112,1868,332],{"class":224},[112,1870,1871],{"class":213}," \"sbc90a16505a04a760bc4a34a0\"",[112,1873,1874],{"class":151}," }",[112,1876,467],{"class":224},[112,1878,1879,1882,1884],{"class":114,"line":303},[112,1880,1881],{"class":155},"      \"event\"",[112,1883,332],{"class":224},[112,1885,597],{"class":151},[112,1887,1888,1891,1893,1895],{"class":114,"line":309},[112,1889,1890],{"class":155},"        \"id\"",[112,1892,332],{"class":224},[112,1894,1871],{"class":213},[112,1896,467],{"class":224},[112,1898,1899,1902,1904,1907],{"class":114,"line":323},[112,1900,1901],{"class":155},"        \"title\"",[112,1903,332],{"class":224},[112,1905,1906],{"class":213}," \"Design Review\"",[112,1908,467],{"class":224},[112,1910,1911,1914,1916,1919],{"class":114,"line":341},[112,1912,1913],{"class":155},"        \"startsAt\"",[112,1915,332],{"class":224},[112,1917,1918],{"class":213}," \"2026-11-10T20:00:00Z\"",[112,1920,467],{"class":224},[112,1922,1923,1926,1928,1931],{"class":114,"line":373},[112,1924,1925],{"class":155},"        \"endsAt\"",[112,1927,332],{"class":224},[112,1929,1930],{"class":213}," \"2026-11-10T21:00:00Z\"",[112,1932,467],{"class":224},[112,1934,1935,1938,1940,1943],{"class":114,"line":378},[112,1936,1937],{"class":155},"        \"startsAtLocal\"",[112,1939,332],{"class":224},[112,1941,1942],{"class":213}," \"2026-11-10T15:00:00-05:00\"",[112,1944,467],{"class":224},[112,1946,1947,1950,1952,1955],{"class":114,"line":397},[112,1948,1949],{"class":155},"        \"endsAtLocal\"",[112,1951,332],{"class":224},[112,1953,1954],{"class":213}," \"2026-11-10T16:00:00-05:00\"",[112,1956,467],{"class":224},[112,1958,1959,1962,1964,1967],{"class":114,"line":403},[112,1960,1961],{"class":155},"        \"timezone\"",[112,1963,332],{"class":224},[112,1965,1966],{"class":213}," \"America\u002FNew_York\"",[112,1968,467],{"class":224},[112,1970,1971,1974,1976,1979],{"class":114,"line":444},[112,1972,1973],{"class":155},"        \"isAllDay\"",[112,1975,332],{"class":224},[112,1977,1978],{"class":282}," false",[112,1980,467],{"class":224},[112,1982,1983,1986,1988,1991],{"class":114,"line":456},[112,1984,1985],{"class":155},"        \"location\"",[112,1987,332],{"class":224},[112,1989,1990],{"class":213}," \"Room 4B\"",[112,1992,467],{"class":224},[112,1994,1995,1998,2000],{"class":114,"line":470},[112,1996,1997],{"class":155},"        \"version\"",[112,1999,332],{"class":224},[112,2001,2002],{"class":213}," \"\\\"1\\\"\"\n",[112,2004,2005],{"class":114,"line":491},[112,2006,2007],{"class":151},"      }\n",[112,2009,2010],{"class":114,"line":516},[112,2011,2012],{"class":151},"    }\n",[112,2014,2015],{"class":114,"line":524},[112,2016,2017],{"class":151},"  ]\n",[112,2019,2020],{"class":114,"line":530},[112,2021,533],{"class":151},[78,2023,2024,2025,2029,2030,332],{},"Show each change with ",[2026,2027,2028],"strong",{},"Approve"," and ",[2026,2031,2032],{},"Decline",[2034,2035,2036,2043],"ul",{},[2037,2038,2039,2042],"li",{},[85,2040,2041],{},"title"," says what the change does.",[2037,2044,2045,2048,2049,2051,2052,634],{},[85,2046,2047],{},"event"," is the event as it is now in Pyas, for a change to an event that already exists. Show its title and time: it's what the change is really about, whatever the model says. A new event has no ",[85,2050,2047],{},", so show the title and times from ",[85,2053,2054],{},"input",[78,2056,2057,2058,2061],{},"Send the IDs the person approved to ",[85,2059,2060],{},"\u002Fchat\u002Fanswer",". The others are declined.",[2063,2064],"screenshot",{"alt":2065,"caption":2066,"height":2067,"src":2068,"width":2069},"A chat. The person asked to move their 1:1 with Sarah to 4:30 PM. A card shows the event's current time and the new time, with Approve and Decline buttons.","One way to show a change: the event's current time from Pyas, and the new time the model asked for.",399,"\u002Fimages\u002Fapproval-in-your-app.png",660,[100,2071,64],{"id":63},[2073,2074,2075,2083,2086,2089],"ol",{},[2037,2076,2077,2078,2080,2081,634],{},"Run your server with a test key, and with ",[85,2079,1750],{}," returning ",[85,2082,1758],{},[2037,2084,2085],{},"Ask \"What's on my calendar tomorrow?\". The reply lists Avery's events, without pausing.",[2037,2087,2088],{},"Ask \"Book a 30 minute meeting tomorrow afternoon when I'm free.\" The agent pauses with one change, \"Create an event\". Approve it, and the reply says it's booked.",[2037,2090,2091],{},"Ask it to delete that meeting, and decline. The meeting stays, and the reply says it was left as it was.",[78,2093,2094,2095,634],{},"To go live, see ",[94,2096,2098],{"href":2097},"\u002Fai-in-your-app#go-live","Go live",[2100,2101,2102],"style",{},"html pre.shiki code .s1IGz, html code.shiki .s1IGz{--shiki-default:var(--shiki-token-function)}html pre.shiki code .sTB4W, html code.shiki .sTB4W{--shiki-default:var(--shiki-token-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sqSWz, html code.shiki .sqSWz{--shiki-default:var(--shiki-foreground)}html pre.shiki code .sag7P, html code.shiki .sag7P{--shiki-default:var(--shiki-token-keyword)}html pre.shiki code .ssgCD, html code.shiki .ssgCD{--shiki-default:var(--shiki-token-comment)}html pre.shiki code .sk1Au, html code.shiki .sk1Au{--shiki-default:var(--shiki-token-string-expression)}html pre.shiki code .sEEt9, html code.shiki .sEEt9{--shiki-default:var(--shiki-token-punctuation)}html pre.shiki code .sXULy, html code.shiki .sXULy{--shiki-default:var(--shiki-token-constant)}",{"title":108,"searchDepth":236,"depth":236,"links":2104},[2105,2106,2107,2108,2109],{"id":51,"depth":166,"text":52},{"id":54,"depth":166,"text":55},{"id":57,"depth":166,"text":58},{"id":60,"depth":166,"text":61},{"id":63,"depth":166,"text":64},"md",false,{},"\u002Flangchain","---\ntitle: LangChain\ndescription: Add a calendar assistant to your app with a LangChain agent and Pyas. It reads each user's Google and Outlook calendars, and pauses for their approval before it books, moves or deletes anything.\nsections:\n  - { id: install, title: Install }\n  - { id: the-agent, title: The agent }\n  - { id: your-routes, title: Your routes }\n  - { id: show-the-change, title: Show the change }\n  - { id: try-it, title: Try it }\n---\n\n# LangChain\n\n::lead\nThis guide adds a calendar assistant to your server with a LangChain agent. The agent uses Pyas's tools for the signed-in user only. When it wants to change a calendar, it pauses until the person approves.\n::\n\nThe code was tested with LangChain 1.6, LangGraph 1.7, and OpenAI's `gpt-5.5` and Anthropic's `claude-sonnet-5-5` as the model. See [AI in your app](\u002Fai-in-your-app) for how the tools work.\n\n## Install\n\n```bash\nnpm install @pyas\u002Fai-tools langchain @langchain\u002Fcore @langchain\u002Flanggraph @langchain\u002Fopenai\n```\n\nSet these on your server:\n\n```bash\nPYAS_API_KEY=pyas_sk_test_...   # a test key from API keys in the Pyas dashboard\nOPENAI_API_KEY=sk-...\n```\n\nFor Claude, install `@langchain\u002Fanthropic`, set `ANTHROPIC_API_KEY`, and use `anthropic:claude-sonnet-5-5` as the model.\n\n## The agent\n\n```ts [assistant.ts]\nimport { createAgent } from 'langchain'\nimport { Command, MemorySaver } from '@langchain\u002Flanggraph'\nimport { pyasToolkit, type PyasApprovalRequest } from '@pyas\u002Fai-tools\u002Flangchain'\n\n\u002F\u002F Keeps each conversation between requests. In production, use a database checkpointer,\n\u002F\u002F such as PostgresSaver from @langchain\u002Flanggraph-checkpoint-postgres.\nconst checkpointer = new MemorySaver()\n\n\u002F** What to show the person: the assistant's reply, or the changes waiting for their approval. *\u002F\nexport type Turn =\n    | { reply: string }\n    | { changes: Array\u003C{ id: string } & PyasApprovalRequest> }\n\nfunction agentFor(userId: string) {\n    \u002F\u002F The same for the whole conversation. Today's date lets the model work out \"tomorrow\".\n    const today = new Date().toISOString().slice(0, 10)\n    return createAgent({\n        model: 'openai:gpt-5.5',\n        tools: pyasToolkit({ subjectId: userId }),\n        systemPrompt: `You help the user manage their calendar. Today is ${today}.`,\n        checkpointer,\n    })\n}\n\nfunction turn(result: { messages: Array\u003C{ text: string }>; __interrupt__?: Array\u003C{ id?: string; value?: unknown }> }): Turn {\n    const waiting = result.__interrupt__ ?? []\n    if (waiting.length) return { changes: waiting.map((change) => ({ id: change.id!, ...(change.value as PyasApprovalRequest) })) }\n    return { reply: result.messages.at(-1)?.text ?? '' }\n}\n\n\u002F** The person sends a message. conversationId is yours: one per conversation. *\u002F\nexport async function ask(conversationId: string, userId: string, text: string): Promise\u003CTurn> {\n    const config = { configurable: { thread_id: conversationId } }\n    return turn(await agentFor(userId).invoke({ messages: [{ role: 'user', content: text }] }, config))\n}\n\n\u002F** The person answers the changes: the ids they approved. Every other change is declined. *\u002F\nexport async function answer(conversationId: string, userId: string, changeIds: string[], approved: string[]): Promise\u003CTurn> {\n    const config = { configurable: { thread_id: conversationId } }\n    const resume = Object.fromEntries(changeIds.map((id) => [id, { approved: approved.includes(id) }]))\n    return turn(await agentFor(userId).invoke(new Command({ resume }), config))\n}\n```\n\nA tool that changes a calendar pauses the agent with LangGraph's `interrupt()`. The agent needs a checkpointer to pause: it saves the conversation, and the agent carries on from the same point once the person approves or declines. Outside an agent, such a tool fails rather than run without approval.\n\n`answer` resumes the agent with `{ approved: true }` or `{ approved: false }` for each change, by the change's ID. A declined change is never sent to Pyas, and the model is told.\n\nKeep the system prompt the same for the whole conversation. Some models, such as Claude, reject a conversation whose system prompt changes partway, so put today's date in it, not the time.\n\n## Your routes\n\nYour server keeps each person's conversation ID and the changes waiting for them. With Express:\n\n```ts [server.ts]\nimport express from 'express'\nimport { randomUUID } from 'node:crypto'\nimport { answer, ask } from '.\u002Fassistant.js'\n\n\u002F** The signed-in person's ID in your app, from your sign-in (here, the sandbox's Avery Brooks). *\u002F\nfunction currentUserId(_req: express.Request): string | null {\n    return 'sandbox_user_1'\n}\n\n\u002F\u002F Each person's conversation and the changes waiting for them. In production, keep these in your database.\nconst conversations = new Map\u003Cstring, { id: string; waiting: string[] }>()\n\nconst app = express()\napp.use(express.json())\n\napp.post('\u002Fchat', async (req, res) => {\n    const userId = currentUserId(req)\n    if (!userId) return res.status(401).json({ error: 'Sign in to use your calendar.' })\n    const conversation = conversations.get(userId) ?? { id: randomUUID(), waiting: [] }\n    conversations.set(userId, conversation)\n    const turn = await ask(conversation.id, userId, req.body.text)\n    conversation.waiting = 'changes' in turn ? turn.changes.map((change) => change.id) : []\n    res.json(turn)\n})\n\n\u002F\u002F The person's answer: { \"approved\": [\"...\"] }. Changes left out are declined.\napp.post('\u002Fchat\u002Fanswer', async (req, res) => {\n    const userId = currentUserId(req)\n    if (!userId) return res.status(401).json({ error: 'Sign in to use your calendar.' })\n    const conversation = conversations.get(userId)\n    if (!conversation?.waiting.length) return res.status(409).json({ error: 'No change is waiting for an answer.' })\n    const turn = await answer(conversation.id, userId, conversation.waiting, req.body.approved ?? [])\n    conversation.waiting = 'changes' in turn ? turn.changes.map((change) => change.id) : []\n    res.json(turn)\n})\n\napp.listen(3457)\n```\n\n`currentUserId` stands for your sign-in. It returns the ID you saved as `subjectId` when the person connected their calendar. To try the guide in the sandbox, have it return `'sandbox_user_1'`, the sandbox person Avery Brooks.\n\n## Show the change\n\nWhen the agent wants to change something, `ask` returns the changes instead of a reply:\n\n```json\n{\n  \"changes\": [\n    {\n      \"id\": \"c9d154673bb91cf774e8a964e2891f8c\",\n      \"type\": \"pyas_approval\",\n      \"tool\": \"delete_event\",\n      \"title\": \"Delete an event\",\n      \"input\": { \"accountId\": \"sbx_f7bb6446c0_1\", \"eventId\": \"sbc90a16505a04a760bc4a34a0\" },\n      \"event\": {\n        \"id\": \"sbc90a16505a04a760bc4a34a0\",\n        \"title\": \"Design Review\",\n        \"startsAt\": \"2026-11-10T20:00:00Z\",\n        \"endsAt\": \"2026-11-10T21:00:00Z\",\n        \"startsAtLocal\": \"2026-11-10T15:00:00-05:00\",\n        \"endsAtLocal\": \"2026-11-10T16:00:00-05:00\",\n        \"timezone\": \"America\u002FNew_York\",\n        \"isAllDay\": false,\n        \"location\": \"Room 4B\",\n        \"version\": \"\\\"1\\\"\"\n      }\n    }\n  ]\n}\n```\n\nShow each change with **Approve** and **Decline**:\n\n- `title` says what the change does.\n- `event` is the event as it is now in Pyas, for a change to an event that already exists. Show its title and time: it's what the change is really about, whatever the model says. A new event has no `event`, so show the title and times from `input`.\n\nSend the IDs the person approved to `\u002Fchat\u002Fanswer`. The others are declined.\n\n::screenshot{src=\"\u002Fimages\u002Fapproval-in-your-app.png\" alt=\"A chat. The person asked to move their 1:1 with Sarah to 4:30 PM. A card shows the event's current time and the new time, with Approve and Decline buttons.\" width=\"660\" height=\"399\" caption=\"One way to show a change: the event's current time from Pyas, and the new time the model asked for.\"}\n::\n\n## Try it\n\n1. Run your server with a test key, and with `currentUserId` returning `'sandbox_user_1'`.\n2. Ask \"What's on my calendar tomorrow?\". The reply lists Avery's events, without pausing.\n3. Ask \"Book a 30 minute meeting tomorrow afternoon when I'm free.\" The agent pauses with one change, \"Create an event\". Approve it, and the reply says it's booked.\n4. Ask it to delete that meeting, and decline. The meeting stays, and the reply says it was left as it was.\n\nTo go live, see [Go live](\u002Fai-in-your-app#go-live).\n",{"title":47,"description":48},null,"IQI2vEArDxDdT8c6qB4mcSRscZG6YFxQeLx31vjp948",1794697271275]