Zbuduj zewnętrznego agenta
Ten przewodnik pokazuje cały proces tworzenia niestandardowego agenta zewnętrznego („przynieś własnego agenta”) dla monday.com: od utworzenia go programowo i wskazania adresu webhooka, przez weryfikację żądań, aż po - co sprawia najwięcej problemów - prawidłowe wysyłanie odpowiedzi, żeby wyświetlały się na czacie agenta.
Funkcja przedpremierowa. API agentów jest obecnie dostępne tylko w wersji API dev (przedpremierowej). Nie jest jeszcze dostępne w żadnej wersji API oznaczonej datą. Sygnatury, typy i działanie mogą się zmienić, zanim funkcja trafi do wersji oznaczonej datą. Każde żądanie musi zawierać nagłówek API-Version: dev.
Pełną specyfikację API (wszystkie zapytania, mutacje, typy i wyliczenia) znajdziesz w dokumentacji API agentów. Informacje o tym, jak agenci wpisują się w działanie platformy, znajdziesz w artykule Tworzenie rozwiązań na monday.com z AI.
Pojęcia
monday.com obsługuje dwa rodzaje agentów, dla których nie musisz tworzyć interfejsu użytkownika:
- Agenci - orkiestratorzy pracy oparte na AI, tworzeni i konfigurowani w monday.com (na poziomie osobistym lub konta). Za pomocą API określasz profil, cel, plan, umiejętności, wiedzę i wyzwalacze, a monday.com uruchamia agentów. Zobacz dokumentację API agentów.
- Agenci zewnętrzni - agenci, których hostujesz i których jesteś właścicielem. monday.com nadaje im pełnoprawną tożsamość (można o nich wspominać za pomocą @, przypisywać ich do elementów i wykonywać przez nich działania na tablicach) oraz komunikuje się z Twoją usługą za pomocą podpisanego webhooka. Ten przewodnik dotyczy agentów zewnętrznych.
Agenta zewnętrznego możesz połączyć na dwa sposoby:
- Zarządzany dostawca (np. agent zarządzany przez Claude) - monday.com koordynuje wywołania dostawcy w Twoim imieniu. Połącz go za pomocą asynchronicznej mutacji
connect_external_agent. Ten przewodnik nie dotyczy zarządzanych dostawców. - Agent niestandardowy (webhook) - podajesz adres URL wywołania zwrotnego, na który monday.com wysyła zdarzenia, a Twoja usługa na nie odpowiada. Połącz go za pomocą synchronicznej mutacji
connect_external_agent_sync. Ten przewodnik dotyczy właśnie tego rozwiązania.
Ogólny schemat działania agenta niestandardowego wygląda tak:
monday.com ──(podpisany POST: "agent_triggered")──▶ adres URL wywołania zwrotnego
Twoja usługa ──(strumień SSE / odpowiedź JSON)──────▶ monday.com (odpowiedź)
Twoja usługa ──(GraphQL z tokenem agenta)───────────▶ monday.com (działania na elementach)
Podczas konfiguracji otrzymujesz jednorazowo dwa dane uwierzytelniające:
| Dane uwierzytelniające | Zastosowanie |
|---|---|
signing_secret |
Weryfikacja, czy przychodzące webhooki rzeczywiście pochodzą z monday.com (HMAC). Wartość jest zmieniana przy każdym ustawieniu lub zaktualizowaniu callback_url. |
api_token |
Wykonywanie wywołań GraphQL API monday.com w imieniu agenta. Wymagany do odpowiedzi w trakcie działania, które korzystają z GraphQL (wspomnienie / przypisanie). |
Co widzi użytkownik
monday czeka na odpowiedź webhooka do około 30 sekund. To, co zobaczy użytkownik, zależy od wyzwalacza:
| Wyzwalacz | Na co monday czeka w żądaniu webhooka | Co użytkownik widzi w monday |
|---|---|---|
chat |
Treść odpowiedzi HTTP 200 w ciągu około 30 s (SSE lub JSON) | Treść odpowiedzi HTTP jest odpowiedzią na czacie |
mention |
Potwierdzenie HTTP 200 (treść nie jest wyświetlana w interfejsie) | Odpowiedź w wątku, którą opublikujesz przez GraphQL z tokenem agenta |
assigned |
Potwierdzenie HTTP 200 (treść nie jest wyświetlana w interfejsie) | Aktualizacja (lub wynik innej mutacji), którą opublikujesz przez GraphQL z tokenem agenta |
W przypadku mention / assigned szybko potwierdź żądanie HTTP, a następnie wykonaj widoczne działanie przez GraphQL. W przypadku chat nie zamykaj żądania, dopóki odpowiedź nie będzie gotowa - GraphQL nie może wyświetlać odpowiedzi agenta na czacie.
Wymagania wstępne
- Konto monday.com z włączonymi agentami (funkcja w ramach wczesnego dostępu).
- Token właściciela do tworzenia agenta i zarządzania nim - token użytkownika monday.com (token dostępu OAuth lub osobisty token API). Mutacje zarządzające są wykonywane w kontekście tego użytkownika i zgodnie z jego uprawnieniami.
- Publiczny punkt końcowy HTTPS, do którego monday.com może wysyłać żądania (zobacz Uwagi dotyczące wdrożenia (monday-code)).
- Wszystkie wywołania API agentów muszą zawierać nagłówek
API-Version: dev.
Poniżej znajduje się pomocnicza funkcja GraphQL używana w tym przewodniku:
async function mondayApi(token, query, variables = {}) {
const res = await fetch('https://api.monday.com/v2', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: token, // owner token OR agent api_token
'API-Version': 'dev', // REQUIRED for the agents API
},
body: JSON.stringify({ query, variables }),
});
return res.json();
}
Utwórz agenta
Użyj connect_external_agent_sync z tokenem właściciela.
Ta mutacja jest synchroniczna i powolna (około 25 sekund). Ustaw limit czasu klienta HTTP na co najmniej 40 sekund i pokaż stan ładowania podczas oczekiwania.
mutation ConnectCustomAgent($input: ConnectExternalAgentSyncInput!) {
connect_external_agent_sync(input: $input) {
agent_id
signing_secret
api_token
instructions
}
}
Zmienne:
{
"input": {
"custom": {
"name": "My Custom Agent",
"callback_url": "https://your-public-host.example.com/agent/webhook"
}
}
}
const data = await mondayApi(ownerToken, CONNECT_MUTATION, {
input: { custom: { name, callback_url } },
});
const { agent_id, signing_secret, api_token, instructions } =
data.data.connect_external_agent_sync;
Pola odpowiedzi
| Pole | Uwagi |
|---|---|
agent_id |
Wewnętrzny identyfikator agenta monday.com. |
signing_secret |
Wyświetlany tylko raz. Zapisz go w bezpiecznym miejscu - służy do weryfikowania webhooków. |
api_token |
Wyświetlany tylko raz. Zapisz go w bezpiecznym miejscu - służy do wykonywania działań w imieniu agenta. |
instructions |
Opcjonalne uwagi dotyczące konfiguracji. |
Wartości signing_secret i api_token są zwracane tylko raz, podczas łączenia agenta. Nie można ich później odzyskać. Zapisz je od razu (np. w menedżerze sekretów lub bezpiecznym magazynie monday-code). Aby zmienić signing_secret, zaktualizuj callback_url za pomocą update_custom_agent - otrzymasz nowy sekret. Jeśli zgubisz api_token, musisz wykonać disconnect_external_agent, a następnie połączyć agenta ponownie.
callback_url musi być publicznym adresem HTTPS dostępnym z infrastruktury monday.com. Podczas wykonywania jest ponownie sprawdzany, aby zapobiegać atakom SSRF i DNS rebinding. Nie używaj localhost ani hosta wewnętrznego (zobacz Uwagi dotyczące wdrożenia (monday-code)).
Wolisz użyć API, ale chcesz zacząć w interfejsie? Agenta niestandardowego możesz też utworzyć z poziomu Agenci → Zarządzaj agentami → Dodaj własnego agenta → Agent niestandardowy. Następnie skopiuj dane uwierzytelniające z okna potwierdzenia - są w nim wyświetlane tylko raz.
Ustaw lub zaktualizuj adres URL wywołania zwrotnego
Aby później zmienić nazwę lub adres URL wywołania zwrotnego, użyj update_custom_agent. Możesz wywołać tę mutację tokenem właściciela (przekaż agent_id) albo własnym api_token agenta (pomijając agent_id - monday rozpozna agenta na podstawie tokenu). Dzięki temu agent może zaktualizować swój webhook po wdrożeniu.
Po ustawieniu lub zmianie callback_url odpowiedź zawiera nowy signing_secret (jest zmieniany przy każdej aktualizacji adresu URL wywołania zwrotnego). Zapisz go od razu - poprzedni sekret przestaje działać przy weryfikacji sygnatury. Aktualizacje samej nazwy zwracają signing_secret: null - nie zastępuj zapisanego sekretu wartością null.
Podczas uwierzytelniania tokenem agenta pomijaj agent_id. Przekazanie go razem z tokenem agenta może spowodować błąd autoryzacji. monday rozpoznaje agenta na podstawie tokenu.
Aktualizacja agenta przez samego agenta (zalecane po wdrożeniu)
Uwierzytelnij się za pomocą api_token agenta. Pomiń agent_id.
mutation UpdateCustomAgent($input: UpdateCustomAgentInput!) {
update_custom_agent(input: $input) {
success
signing_secret
}
}
{
"input": {
"callback_url": "https://your-public-host.example.com/agent/webhook"
}
}
Token właściciela
Przekaż jawnie agent_id. Możesz też zaktualizować name.
{
"input": {
"agent_id": "1234567890",
"name": "My Renamed Agent",
"callback_url": "https://your-public-host.example.com/agent/webhook"
}
}
Pola name i callback_url są niezależne i oba opcjonalne. Błędy (nie znaleziono, brak uprawnień, nieprawidłowy adres URL) są zwracane jako błędy GraphQL, a nie success: false. Nowy adres URL musi być prawidłowym, publicznym punktem końcowym HTTPS.
Sprawdź, czy masz token agenta
Integracje, które przyjmują wklejony token, powinny przed zarejestrowaniem adresu wywołania zwrotnego sprawdzić, czy jest to token agenta niestandardowego. Wywołaj me z tym tokenem i akceptuj tylko następujące wartości kind:
me.kind |
Znaczenie |
|---|---|
external_agent_member |
Token agenta niestandardowego (połączonego) |
external_agent_detached_member |
Token agenta niestandardowego (pojawia się też w przypadku nowo utworzonych agentów) |
Odrzucaj tokeny właściciela/użytkownika (admin, member, …) i tokeny agentów osobistych (personal_agent_member).
query {
me {
id
name
kind
email
}
}
Adresy e-mail agentów niestandardowych mają format agent-<CUSTOM_AGENT_ID>@agent.monday.com - w razie potrzeby możesz odczytać z me.email identyfikator agenta na potrzeby wyświetlania lub rejestrowania zdarzeń. Tożsamość najlepiej sprawdzać za pomocą me; wyświetlenie listy custom_agents z tokenem agenta jest działaniem wykonywanym na zasadzie best effort i może się nie powieść.
Aktywuj agenta
Nowo utworzeni agenci są domyślnie nieaktywni. Zanim będą mogli działać, musisz ich aktywować. Użyj activate_agent.
mutation {
activate_agent(id: 1234567890) {
success
}
}
Przyznaj dostęp do tablicy
Połączony agent działa w ramach własnej tożsamości - Twoje osobiste uprawnienia do tablic nie są przenoszone na agenta. Przyznaj mu dostęp do potrzebnych tablic i dokumentów za pomocą add_agent_resource_access.
mutation {
add_agent_resource_access(
id: 1234567890,
resource_id: 9876543210,
scope_type: BOARD, # or DOC
permission_type: READ_WRITE # READ or READ_WRITE
) {
success
}
}
Uprawnienie READ_WRITE jest wymagane do tworzenia i edytowania elementów oraz publikowania aktualizacji.
Dostęp możesz też przyznać w interfejsie, na stronie agenta → Wiedza i dostęp. Jeśli tablica znajduje się w niestandardowym regionie przechowywania danych, token API agenta musi umożliwiać dostęp do tego regionu.
Odbieranie wyzwalaczy (webhook)
Po wyzwoleniu agenta monday.com wysyła podpisane żądanie POST na adres URL wywołania zwrotnego i czeka na odpowiedź w ramach tego samego żądania. Nie ma osobnego tokenu ani adresu URL do wysyłania odpowiedzi - żądanie jest synchroniczne.
Nagłówki
| Nagłówek | Przykład | Zastosowanie |
|---|---|---|
x-monday-agent-id |
139988 |
Identyfikator uruchomionego agenta → znajdź zapisane dla niego dane uwierzytelniające. |
x-monday-signature |
sha256=a3a6ce… |
HMAC treści - zweryfikuj ją. |
x-monday-timestamp |
1782326623754 |
Czas epoki w milisekundach; jest częścią podpisywanego ciągu. |
content-type |
application/json |
Treść żądania jest w formacie JSON. |
Agenci hostowani w monday-code (Cloud Run) otrzymują też nagłówki infrastruktury: x-serverless-authorization (token JWT podpisany przez Google), host (wewnętrzny host …---service-….a.run.app - nie jest to Twój publiczny adres URL; nigdy nie wyznaczaj na jego podstawie adresu wywołania zwrotnego), x-forwarded-*, cf-*, via: 1.1 google i user-agent: node. Do podpisywania i kierowania żądań używaj nagłówków x-monday-* powyżej, a nie tych nagłówków.
Struktura treści żądania
Treść zawsze ma tę samą strukturę; zmienia się tylko payload, zależnie od typu wyzwalacza.
{
"event": "agent_triggered",
"triggerType": "chat",
"payload": { "text": "...", "...": "trigger-specific fields" },
"timestamp": "2026-06-24T18:43:43.754Z",
"stream": true
}
| Pole | Typ | Opis |
|---|---|---|
event |
String |
W przypadku webhooków agentów niestandardowych zawsze ma wartość agent_triggered. |
triggerType |
String |
Sposób wyzwolenia agenta: chat, assigned, mention lub unknown. Mogą pojawić się aliasy, takie jak mentioned / assign - zamień je na standardowe wartości. |
payload |
Object |
Dane zależne od wyzwalacza. Zawsze zawiera text; pozostałe pola zależą od triggerType. |
timestamp |
String |
Czas wystąpienia zdarzenia w formacie ISO 8601. |
stream |
Boolean |
Gdy ma wartość true (domyślną) lub nie jest podane, odpowiedz za pomocą SSE. Gdy ma wartość false, odpowiedz w formacie JSON { "message": "..." }. |
Typy wyzwalaczy
chat
Użytkownik wpisał wiadomość na czacie agenta. Ładunek zawiera minimalną ilość danych i nie zawiera identyfikatorów - tylko tekst. Dlatego odpowiedź na czacie musi być zwrócona w odpowiedzi HTTP: nie ma adresu, na który można by wysłać ją osobno.
{
"event": "agent_triggered",
"triggerType": "chat",
"payload": { "text": "sent this message through agent chat" },
"timestamp": "2026-06-24T18:43:43.754Z",
"stream": true
}
Żądanie czatu może zawierać flagę stream (domyślnie ma wartość true). Zobacz sekcję Odpowiadanie na wyzwalacze.
assigned
Agent został przypisany do elementu. Ładunek zawiera pełne dane docelowe, a monday.com formatuje text jako instrukcję. Ładunek nie zawiera nazwy elementu - jeśli jej potrzebujesz, pobierz ją przez GraphQL.
{
"event": "agent_triggered",
"triggerType": "assigned",
"payload": {
"text": "You were assigned to an item. Follow these steps:\n1. ...\n\nTRIGGER DATA:\n- itemId: 12334011531\n- boardId: 18418747579\n- groupId: topics",
"itemId": 12334011531,
"boardId": 18418747579,
"groupId": "topics",
"updateId": null,
"replyId": null,
"updateBody": null,
"files": null
},
"timestamp": "2026-06-24T18:45:26.706Z"
}
Szybko potwierdź odbiór webhooka, a następnie wykonaj działanie na elemencie przez API (np. użyj create_update dla itemId). Treść odpowiedzi HTTP nie jest wyświetlana w interfejsie.
mention
Agent został wspomniany za pomocą @ w aktualizacji. Agent otrzyma webhook tylko wtedy, gdy ma dostęp do odpowiedniej tablicy i wzmianka pojawia się w prawidłowym kontekście.
{
"event": "agent_triggered",
"triggerType": "mention",
"payload": {
"text": "<monday-generated instruction prompt>",
"itemId": 123456789,
"boardId": 987654321,
"updateId": 111222333,
"updateBody": "user update text where @agent was mentioned",
"replyId": null,
"files": null
},
"timestamp": "2026-06-24T18:46:10.000Z"
}
Przygotowując odpowiedź, preferuj payload.updateBody, bo zawiera tekst użytkownika - payload.text to często instrukcja wygenerowana przez monday, a nie treść napisana przez użytkownika. Potwierdź odbiór webhooka, a następnie opublikuj odpowiedź w wątku za pomocą create_update z item_id + parent_id (= updateId). Zobacz sekcję Wykonywanie działań w imieniu agenta przez API.
Limity: limit czasu żądania wynosi 30 sekund; treść odpowiedzi może mieć maksymalnie 1 MB.
Weryfikacja podpisu żądania
Przed przetworzeniem zweryfikuj każde przychodzące żądanie. Oblicz HMAC-SHA256 dla ciągu ${timestamp}.${rawBody} przy użyciu signing_secret, dodaj przed skrótem szesnastkowym prefiks sha256= i porównaj wynik z x-monday-signature za pomocą porównania w stałym czasie.
import crypto from 'crypto';
function verifySignature(signingSecret, rawBody, headers) {
const timestamp = headers['x-monday-timestamp'];
const received = headers['x-monday-signature'];
if (!timestamp || !received) return false;
const expected = 'sha256=' + crypto
.createHmac('sha256', signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(received);
// timingSafeEqual throws on length mismatch - guard first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Oblicz HMAC na nieprzetworzonych bajtach treści żądania, przed ich analizą. Jeśli ponownie zserializujesz przeanalizowane req.body, kolejność kluczy i białe znaki będą inne, więc weryfikacja się nie powiedzie. Zachowaj surową treść, np. za pomocą funkcji verify w express.json({ verify }). Użyj tutaj signing_secret - nie api_token.
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; },
}));
Odpowiadanie na wyzwalacze
monday odczytuje odpowiedź agenta synchronicznie z odpowiedzi HTTP. Sposób działania zależy od wyzwalacza - zobacz sekcję Co widzi użytkownik.
| Wyzwalacz | Zalecane działanie HTTP |
|---|---|
chat |
Nie zamykaj żądania; zwróć SSE lub JSON (poniżej). To jedyny sposób na odpowiedź na czacie agenta. |
mention / assigned |
Szybko zwróć HTTP 200 z pustym potwierdzeniem SSE (albo pustym JSON, jeśli stream: false), a następnie wykonuj dalsze działania przez GraphQL. |
Puste potwierdzenie SSE (w przypadku mention / assigned):
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: [DONE]
Jeśli stream: false, potwierdź odbiór za pomocą { "message": "" }.
Zwykła odpowiedź JSON, np. { "text": "..." }, dla wyzwalacza czatu nie wyświetli się - przebieg zakończy się komunikatem „Coś poszło nie tak podczas uruchamiania agenta”. Musisz przesyłać strumień SSE (albo użyć formatu JSON bez strumieniowania opisanego poniżej).
Odpowiedź strumieniowana (domyślna - używana na czacie)
- Zwróć status
200i nagłówekContent-Type: text/event-stream. - Wyślij co najmniej jedno zdarzenie:
data: {"type":"text","content":"<fragment>"}, a po nim pusty wiersz. - Zakończ strumień przez wysłanie
data: [DONE], a po nim pustego wiersza. - monday.com łączy fragmenty
contentw wiadomość na czacie.
function streamReply(res, replyText) {
res.status(200);
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
if (res.flushHeaders) res.flushHeaders();
const tokens = replyText.split(' ');
let i = 0, closed = false;
res.req.on('close', () => { closed = true; });
const send = () => {
if (closed) return;
if (i < tokens.length) {
const isLast = i === tokens.length - 1;
const content = tokens[i] + (isLast ? '' : ' ');
i += 1;
res.write(`data: ${JSON.stringify({ type: 'text', content })}\n\n`);
setTimeout(send, 40); // pace the stream; optional
} else {
res.write('data: [DONE]\n\n');
res.end();
}
};
send();
}
Możesz też wysłać całą wiadomość w jednym fragmencie, a następnie [DONE] - przesyłanie tekstu token po tokenie wygląda po prostu lepiej.
Odpowiedź bez strumieniowania
Jeśli treść żądania zawiera stream: false, zwróć pojedynczy obiekt JSON:
{ "message": "your full reply text" }
Zasady
- Odpowiedz w ciągu 30 sekund, ze statusem 200 i treścią o rozmiarze nie większym niż 1 MB. Cała operacja - łącznie z pełnym strumieniem SSE - musi zakończyć się w tym czasie. W przeciwnym razie przebieg zakończy się przekroczeniem limitu czasu i nie będzie można na niego później odpowiedzieć.
- Zdarzenia
event-streamsą oddzielone pustym wierszem (\n\n). - W przypadku
chatwygenerujreplyTextza pomocą swojego LLM i zwróć go w treści odpowiedzi HTTP. W przypadkumention/assignedpotwierdź odbiór żądania HTTP i wykonaj widoczne działanie przez API.
Wykonywanie działań w imieniu agenta przez API
W przypadku wyzwalaczy zawierających element docelowy (assigned, mention) - lub zawsze wtedy, gdy agent ma coś zrobić - wywołaj GraphQL API za pomocą api_token agenta i nagłówka API-Version: dev. Działania są wykonywane w imieniu agenta i zgodnie z jego uprawnieniami.
Opublikuj aktualizację przy przypisanym elemencie:
await mondayApi(agentApiToken, `
mutation ($itemId: ID!, $body: String!) {
create_update(item_id: $itemId, body: $body) { id }
}`,
{ itemId: String(payload.itemId), body: 'On it - reviewing the item now.' }
);
Odpowiedz w wątku wzmianki (parent_id = payload.updateId):
mutation ($itemId: ID!, $body: String!, $parentId: ID) {
create_update(item_id: $itemId, body: $body, parent_id: $parentId) {
id
text_body
}
}
{
"itemId": "123456789",
"parentId": "111222333",
"body": "Thanks - I saw your mention and will follow up."
}
Sprawdź poprawność tokenu i tożsamość:
curl -X POST https://api.monday.com/v2 \
-H "Authorization: <agent-api-token>" \
-H "API-Version: dev" \
-H "Content-Type: application/json" \
-d '{"query":"{ me { id name kind email account { id } } }"}'
Pamiętaj: agent musi mieć jawny dostęp do tablicy (Przyznaj dostęp do tablicy). W przeciwnym razie operacje zapisu się nie powiodą, nawet jeśli token jest prawidłowy.
Odłącz agenta
Ta operacja unieważnia token agenta i usuwa agenta. Użyj tokenu właściciela. Zobacz disconnect_external_agent.
mutation {
disconnect_external_agent(id: 1234567890) {
success
}
}
Aby zmienić utracone dane uwierzytelniające, odłącz agenta, a następnie połącz go ponownie.
Pełna implementacja referencyjna (Express)
Minimalny odbiornik webhooków od początku do końca: weryfikacja → analiza → odpowiedź (SSE) → (opcjonalnie) działanie.
import express from 'express';
import crypto from 'crypto';
const app = express();
// Capture the RAW body for signature verification.
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
// Look these up per agent_id from your secure store (set at connect time).
async function getAgentCreds(agentId) {
// return { name, signingSecret, apiToken } | null
}
function verifySignature(signingSecret, rawBody, headers) {
const ts = headers['x-monday-timestamp'];
const recv = headers['x-monday-signature'];
if (!ts || !recv) return false;
const expected = 'sha256=' + crypto.createHmac('sha256', signingSecret)
.update(`${ts}.${rawBody}`).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(recv);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
async function mondayApi(token, query, variables = {}) {
const res = await fetch('https://api.monday.com/v2', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: token, 'API-Version': 'dev' },
body: JSON.stringify({ query, variables }),
});
return res.json();
}
app.post('/agent/webhook', async (req, res) => {
const agentId = req.headers['x-monday-agent-id'];
const rawBody = req.rawBody ? req.rawBody.toString('utf8') : JSON.stringify(req.body || {});
const body = req.body || {};
const triggerType = String(body.triggerType || 'unknown').toLowerCase();
const normalized =
triggerType === 'mentioned' ? 'mention' :
triggerType === 'assign' ? 'assigned' :
triggerType;
const wantsStream = body.stream !== false; // default: stream
const payload = body.payload || {};
if (!agentId) return res.status(400).json({ error: 'missing x-monday-agent-id' });
const creds = await getAgentCreds(agentId);
if (creds?.signingSecret && !verifySignature(creds.signingSecret, rawBody, req.headers)) {
return res.status(401).json({ error: 'invalid signature' });
}
// --- chat: keep the request open and return the reply in the HTTP body ---
if (normalized === 'chat') {
const replyText = `Hi! I'm ${creds?.name || 'the agent'}. You said: "${payload.text || ''}"`;
if (!wantsStream) return res.status(200).json({ message: replyText });
res.status(200);
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders?.();
res.write(`data: ${JSON.stringify({ type: 'text', content: replyText })}\n\n`);
res.write('data: [DONE]\n\n');
return res.end();
}
// --- mention / assigned: ack immediately, then act via GraphQL ---
if (wantsStream) {
res.status(200);
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders?.();
res.write('data: [DONE]\n\n');
res.end();
} else {
res.status(200).json({ message: '' });
}
if (!creds?.apiToken || !payload.itemId) return;
if (normalized === 'mention' && payload.updateId) {
const userText = payload.updateBody || payload.text || '';
await mondayApi(creds.apiToken, `
mutation ($itemId: ID!, $body: String!, $parentId: ID) {
create_update(item_id: $itemId, body: $body, parent_id: $parentId) { id }
}`, {
itemId: String(payload.itemId),
parentId: String(payload.updateId),
body: `Thanks - I saw your mention: "${userText}"`,
});
}
if (normalized === 'assigned') {
await mondayApi(creds.apiToken, `
mutation ($itemId: ID!, $body: String!) {
create_update(item_id: $itemId, body: $body) { id }
}`, {
itemId: String(payload.itemId),
body: 'On it - reviewing the item now.',
});
}
});
app.listen(process.env.PORT || 8080);
Uwagi dotyczące wdrożenia (monday-code)
Jeśli hostujesz agenta w monday-code, najczęstszym źródłem problemów jest użycie nieprawidłowego hosta jako adresu URL wywołania zwrotnego.
- Za pośrednictwem proxy monday-code host żądania Twojego serwera lub
req.hostnamew Express to wewnętrzny host Cloud Run, np.xxxxx---service-….a.run.app. Ten host zwraca monday.com odpowiedź403 Forbidden- webhooki nigdy nie docierają. - Zawsze używaj publicznego hosta:
https://<deploy>-service-…-<region>.monday.app/.... - Ustal go na podstawie
originfrontendu (iframe jest serwowany z publicznego hosta), nagłówkax-forwarded-hostalbo jawnej zmiennejPUBLIC_BASE_URL- i bezwzględnie odrzucaj każdą wartość*.run.app. - Publiczny host wdrożenia zmienia się przy każdym wdrożeniu. Jeśli ustawisz adres wywołania zwrotnego na konkretny host wdrożenia, musisz zaktualizować go po każdym wdrożeniu. Zamiast tego użyj stabilnego aliasu live (
https://live1-service-…monday.app/...), aby adres wywołania zwrotnego nie zmieniał się po ponownym wdrożeniu. - Adresy URL OAuth/przekierowań (jeśli aplikacja korzysta z OAuth) muszą uwzględniać każdy publiczny host, z którego korzystasz.
Rozwiązywanie problemów
| Objaw | Przyczyna / rozwiązanie |
|---|---|
| „Coś poszło nie tak podczas uruchamiania agenta”. | Zwrócono zwykłą odpowiedź JSON dla wyzwalacza chat. Odpowiedz przez SSE (text/event-stream, fragmenty {type:"text",content:…}, data: [DONE]). |
| Odpowiedź na czacie się nie wyświetla | GraphQL nie może wyświetlić odpowiedzi na czacie - odpowiedź znajduje się w treści odpowiedzi HTTP. Uwzględnij też wartość stream (SSE lub { "message": "..." }). |
| Odpowiedź na wzmiankę zawiera niewłaściwy tekst | Preferuj payload.updateBody (aktualizację użytkownika). payload.text to często instrukcja monday. |
| Webhook nie dociera | Adres wywołania zwrotnego wskazuje niedostępny host: *.run.app (zwraca 403), localhost albo adres http. Użyj publicznego hosta HTTPS *.monday.app. |
| Weryfikacja podpisu się nie udaje | HMAC musi być obliczony dla ${timestamp}.${rawBody} z użyciem surowej treści i signing_secret (nie api_token). Po zaktualizowaniu callback_url użyj nowego signing_secret z odpowiedzi. |
Błąd autoryzacji przy update_custom_agent z tokenem agenta |
Jeśli używasz tokenu agenta, pomiń agent_id. Przekazuj agent_id tylko z tokenem właściciela. |
| Zapisany sekret podpisu został wyczyszczony po zmianie nazwy | Aktualizacje samej nazwy zwracają signing_secret: null - zastępuj zapisany sekret tylko wtedy, gdy zwrócona wartość nie jest null. |
| Agent „ma dostęp, ale nie może edytować elementów” | Przyznaj uprawnienie READ_WRITE za pomocą add_agent_resource_access - Twoje własne uprawnienia nie są przenoszone na agenta. |
| Agent się nie uruchamia | Jest nieaktywny - wywołaj activate_agent. |
| Utracono sekrety | api_token jest wyświetlany tylko raz, podczas łączenia agenta. Aby zmienić signing_secret, wywołaj update_custom_agent z nowym callback_url. Aby zmienić oba sekrety, wykonaj disconnect_external_agent, a następnie połącz agenta ponownie. |
Upłynął limit czasu connect_external_agent_sync |
Mutacja jest synchroniczna (około 25 s). Ustaw limit czasu klienta na co najmniej 40 s. |
Skrócona dokumentacja
CREATE connect_external_agent_sync(input:{custom:{name, callback_url}}) → agent_id, signing_secret, api_token [owner token, ~25s]
UPDATE update_custom_agent(input:{agent_id?, name?, callback_url?}) → success, signing_secret? [owner token or agent api_token; omit agent_id with agent token; callback_url rotates signing_secret; name-only → signing_secret null]
ACTIVATE activate_agent(id) → success [owner token]
ACCESS add_agent_resource_access(id, resource_id, scope_type, permission_type) → success [owner token]
DELETE disconnect_external_agent(id) → success [owner token]
IDENTITY me { id name kind email } → kind: external_agent(_detached)_member [agent token]
WEBHOOK POST {callback_url}
headers: x-monday-agent-id, x-monday-signature, x-monday-timestamp
body: { event:"agent_triggered", triggerType, payload:{text, ...}, timestamp, stream? }
verify: HMAC-SHA256(signing_secret, `${timestamp}.${rawBody}`) === x-monday-signature
chat: SSE data: {"type":"text","content":"..."}\n\n ... data: [DONE]\n\n
(or { "message": "..." } when stream:false)
mention/assigned: ack with empty SSE/[DONE] (or { "message": "" }), then GraphQL as agent
ACT POST https://api.monday.com/v2 Authorization: {api_token} API-Version: dev [act as the agent]
mention: create_update(item_id, body, parent_id=updateId) - prefer payload.updateBody for user text
assigned: create_update(item_id, body)
Wszystkie wywołania API agentów wymagają nagłówka API-Version: dev, dopóki funkcja jest przedpremierowa.
📘 Dołącz do społeczności deweloperów!
Jeśli masz pytania lub potrzebujesz pomocy z agentami niestandardowymi, odwiedź społeczność deweloperów monday.com.