Sesi adalah interaksi yang berjalan lama. Meskipun sebagian besar interaksi real-time terjadi melalui aliran peristiwa SSE, webhook memberi tahu Anda tentang perubahan status yang penting.
Peristiwa webhook mengembalikan type dan id peristiwa, bukan objek lengkapnya. Saat Anda menerima peristiwa webhook, Anda perlu mengambil objek tersebut secara langsung dengan panggilan GET. Hal ini menghindari pengiriman data usang saat percobaan ulang dan menjaga setiap pengiriman tetap kecil.
Jenis peristiwa yang didukung
Peristiwa sesi
Beberapa peristiwa ini memiliki nama yang berbeda dari peristiwa yang sesuai pada aliran peristiwa sesi. Misalnya, session.status_idle dan session.status_running pada aliran sesuai dengan peristiwa webhook session.status_idled dan session.status_run_started.
| Peristiwa | Pemicu |
|---|---|
session.status_run_started | Eksekusi agen dimulai. Ini terpicu pada setiap transisi status sesi ke running. |
session.status_idled | Agen menunggu input, misalnya persetujuan izin alat atau pesan pengguna baru. |
session.budget_reached | Sesi mencapai anggarannya dan dijeda. Terpicu paling banyak satu kali untuk setiap nilai anggaran yang Anda tetapkan; mengubah anggaran akan mengaktifkannya kembali. |
session.status_rescheduled | Terjadi kesalahan sementara dan sesi sedang mencoba ulang secara otomatis. |
session.status_terminated | Sesi dihentikan, baik karena kesalahan yang tidak dapat dipulihkan maupun karena diarsipkan. |
session.thread_created | Thread multiagen baru dibuka: agen tambahan yang dipanggil oleh koordinator mulai bekerja, atau advisor sesi sedang dikonsultasikan. |
session.thread_idled | Sebuah agen dalam interaksi multiagen sedang menunggu input. |
session.thread_terminated | Sebuah thread multiagen dihentikan, baik karena thread tersebut diarsipkan maupun karena telah menghabiskan percobaan ulangnya. Anak yang dibuat oleh koordinator dan telah menyelesaikan pekerjaannya menjadi idle, bukan terminated (thread advisor dihentikan setelah konsultasinya selesai). Hanya terpicu untuk thread anak; akhir dari thread utama, termasuk pengarsipan seluruh sesi, hanya muncul sebagai session.status_terminated. |
session.outcome_evaluation_ended | Evaluasi hasil untuk satu iterasi selesai. |
session.updated | Properti sesi berubah (misalnya, nama atau konfigurasinya diperbarui). |
session.deleted | Sesi dihapus secara permanen. Tidak ada objek yang tersisa untuk diambil, jadi perlakukan peristiwa itu sendiri sebagai final. |
Peristiwa vault
| Peristiwa | Pemicu |
|---|---|
vault.created | Vault dibuat. |
vault.archived | Vault diarsipkan. Peristiwa vault_credential.archived juga dipancarkan untuk setiap kredensial di dalamnya. |
vault.deleted | Vault dihapus. Peristiwa vault_credential.deleted juga dipancarkan untuk setiap kredensial di dalamnya. Tidak ada objek yang tersisa untuk diambil, jadi perlakukan peristiwa itu sendiri sebagai final. |
vault_credential.created | Kredensial dibuat. |
vault_credential.archived | Kredensial diarsipkan, baik secara langsung maupun sebagai akibat dari pengarsipan vault. |
vault_credential.deleted | Kredensial dihapus, baik secara langsung maupun sebagai akibat dari penghapusan vault. Tidak ada objek yang tersisa untuk diambil, jadi perlakukan peristiwa itu sendiri sebagai final. |
vault_credential.refresh_failed | Kredensial mcp_oauth tidak dapat diperbarui (refresh token tidak valid, atau kesalahan yang tidak dapat dipulihkan dari server OAuth). |
Peristiwa agen
Peristiwa ini melacak siklus hidup sumber daya agen di workspace Anda, dan berbeda dari peristiwa agen yang dikirimkan pada aliran peristiwa sesi.
| Peristiwa | Pemicu |
|---|---|
agent.created | Agen dibuat. |
agent.updated | Versi baru agen dipublikasikan. Pembaruan yang tidak membuat versi baru tidak memicu peristiwa ini. |
agent.archived | Agen diarsipkan. |
agent.deleted | Agen dihapus secara permanen. Tidak ada objek yang tersisa untuk diambil, jadi perlakukan peristiwa itu sendiri sebagai final. |
Peristiwa deployment
| Peristiwa | Pemicu |
|---|---|
deployment.created | Deployment terjadwal dibuat. |
deployment.updated | Properti deployment berubah (misalnya, jadwalnya diperbarui). |
deployment.paused | Deployment dijeda, baik atas permintaan maupun secara otomatis ketika eksekusi terjadwal gagal dengan kesalahan yang tidak dapat dipulihkan, seperti subagen yang diarsipkan atau environment yang diarsipkan. Kegagalan yang dapat dipulihkan, termasuk batas laju, tidak menjeda deployment. Lihat Perilaku kegagalan. |
deployment.unpaused | Jeda deployment dibatalkan, melanjutkan jadwalnya. |
deployment.archived | Deployment diarsipkan, baik secara langsung maupun karena agennya diarsipkan. Jika agen dihapus, deployment terjadwal akan diarsipkan pada eksekusi terjadwal berikutnya; deployment tanpa jadwal tidak diarsipkan secara otomatis. |
deployment.deleted | Deployment dihapus secara permanen. Tidak ada objek yang tersisa untuk diambil, jadi perlakukan peristiwa itu sendiri sebagai final. |
Peristiwa eksekusi deployment
| Peristiwa | Pemicu |
|---|---|
deployment_run.started | Eksekusi terjadwal dimulai. Hanya eksekusi terjadwal yang memancarkan peristiwa deployment_run; eksekusi manual tidak. |
deployment_run.succeeded | Eksekusi terjadwal berhasil membuat sesinya. Peristiwa ini membawa data.id (ID eksekusi) yang sama dengan peristiwa deployment_run.started dari eksekusi tersebut. Untuk mengikuti pekerjaan sesi, berlanggananlah ke peristiwa sesinya (tab Peristiwa sesi), atau ambil eksekusi deployment untuk mendapatkan session_id-nya. |
deployment_run.failed | Eksekusi terjadwal tidak membuat sesi. Peristiwa ini membawa data.id yang sama dengan peristiwa deployment_run.started dari eksekusi tersebut. Ambil eksekusi deployment untuk detail kesalahannya. |
Peristiwa environment
| Peristiwa | Pemicu |
|---|---|
environment.created | Environment dibuat. |
environment.updated | Environment diperbarui dengan setidaknya satu field yang berubah. Pembaruan tanpa perubahan (no-op) tidak memancarkan apa pun. |
environment.archived | Environment diarsipkan. Mengarsipkan ulang environment yang sudah diarsipkan tidak memancarkan apa pun. |
environment.deleted | Environment dihapus, termasuk penghapusan environment yang sudah diarsipkan. Tidak ada objek yang tersisa untuk diambil, jadi perlakukan peristiwa itu sendiri sebagai final. |
Work item milik environment tidak memancarkan peristiwa webhook.
Peristiwa memory store
| Peristiwa | Pemicu |
|---|---|
memory_store.created | Memory store dibuat, baik oleh Anda maupun oleh proses yang dioperasikan Juglow yang mengkloning salah satu store Anda yang sudah ada. |
memory_store.archived | Memory store diarsipkan. Mengarsipkan ulang store yang sudah diarsipkan tidak memancarkan apa pun. |
memory_store.deleted | Memory store dihapus, termasuk penghapusan store yang sudah diarsipkan. Menghapus store akan berjenjang ke memori dan versi memorinya tanpa memancarkan peristiwa per memori; satu peristiwa memory_store.deleted adalah sinyalnya. Tidak ada objek yang tersisa untuk diambil, jadi perlakukan peristiwa itu sendiri sebagai final. |
Memori individual dan versi memori tidak memancarkan peristiwa webhook.
Mendaftarkan endpoint
Kunjungi Manage > Webhooks di Haijun Console.
Sebuah endpoint webhook terdiri dari:
- URL: Harus HTTPS pada port 443 dengan hostname yang dapat di-resolve secara publik.
- Jenis peristiwa: Daftar nilai
data.typeyang diterima endpoint ini. Sebuah endpoint hanya menerima peristiwa yang dilangganinya.
- Signing secret: Secret 32-byte berawalan
whsec_yang dihasilkan saat pembuatan. Secret ini hanya ditampilkan sekali, jadi simpan dengan aman untuk memverifikasi pengiriman webhook.
Memverifikasi tanda tangan
Setiap pengiriman membawa header webhook-id, webhook-timestamp, dan webhook-signature. Gunakan helper unwrap() (csharp, go: Unwrap()) dari SDK untuk memverifikasi tanda tangan dan mem-parsing peristiwa dalam satu langkah. Helper ini akan melempar kesalahan jika tanda tangan tidak valid atau payload berusia lebih dari 5 menit.
Atur JUGLOW_WEBHOOK_SIGNING_KEY ke secret berawalan whsec_ yang ditampilkan saat pembuatan endpoint.
from flask import Flask, request
import juglow
client = juglow.Juglow() # reads JUGLOW_WEBHOOK_SIGNING_KEY from env
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def webhook():
try:
# unwrap() memunculkan error jika tanda tangan tidak valid atau payload sudah kedaluwarsa
event = client.beta.webhooks.unwrap(
request.get_data(as_text=True),
headers=dict(request.headers),
)
except Exception:
return "invalid signature", 400
if event.data.type == "session.status_idled":
print("session idled:", event.data.id)
# tangani jenis event lainnya
return "", 200 import express from "express";
import Juglow from "@juglow-ai/sdk";
const client = new Juglow(); // reads JUGLOW_WEBHOOK_SIGNING_KEY from env
const app = express();
// PENTING: gunakan express.raw(), bukan express.json(). Tanda tangan dihitung dari byte mentah.
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
// unwrap() melempar error jika tanda tangan tidak valid atau payload sudah kedaluwarsa
event = client.beta.webhooks.unwrap(req.body.toString("utf8"), {
headers: req.headers as Record<string, string>
});
} catch {
return res.status(400).send("invalid signature");
}
switch (event.data.type) {
case "session.status_idled":
console.log("session idled:", event.data.id);
break;
// tangani jenis event lainnya
}
res.sendStatus(200);
}); using Juglow;
var client = new JuglowClient(); // reads JUGLOW_WEBHOOK_SIGNING_KEY from env
var app = WebApplication.Create(args);
app.MapPost("/webhook", async (HttpRequest request) =>
{
using var reader = new StreamReader(request.Body);
var body = await reader.ReadToEndAsync();
var headers = request.Headers.ToDictionary(header => header.Key, header => header.Value.ToString());
UnwrapWebhookEvent webhookEvent;
try
{
// Unwrap() melempar error jika tanda tangan tidak valid atau payload sudah kedaluwarsa
webhookEvent = client.Beta.Webhooks.Unwrap(body, headers);
}
catch
{
return Results.BadRequest("invalid signature");
}
if (webhookEvent.Data.TryPickSessionStatusIdled(out var idled))
{
Console.WriteLine($"session idled: {idled.ID}");
}
// tangani jenis event lainnya
return Results.Ok();
}); package main
import (
"fmt"
"io"
"net/http"
"github.com/juglows/juglow-sdk-go"
)
var client = juglow.NewClient() // reads JUGLOW_WEBHOOK_SIGNING_KEY from env
func webhook(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "could not read body", http.StatusBadRequest)
return
}
// Unwrap mengembalikan error jika tanda tangan tidak valid atau payload sudah kedaluwarsa
event, err := client.Beta.Webhooks.Unwrap(body, r.Header)
if err != nil {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
switch event.Data.Type {
case "session.status_idled":
fmt.Println("session idled:", event.Data.ID)
// tangani jenis event lainnya
}
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/webhook", webhook)
} import com.juglow.client.JuglowClient;
import com.juglow.client.okhttp.JuglowOkHttpClient;
import com.juglow.core.UnwrapWebhookParams;
import com.juglow.core.http.Headers;
import com.sun.net.httpserver.HttpServer;
// membaca JUGLOW_WEBHOOK_SIGNING_KEY dari env
JuglowClient client = JuglowOkHttpClient.fromEnv();
void main() throws Exception {
var server = HttpServer.create(new InetSocketAddress(8000), 0);
server.createContext("/webhook", exchange -> {
var body = new String(exchange.getRequestBody().readAllBytes());
var headers = Headers.builder();
exchange.getRequestHeaders().forEach(headers::put);
try {
// unwrap() melempar error jika tanda tangan tidak valid atau payload sudah kedaluwarsa
var event = client.beta().webhooks().unwrap(
UnwrapWebhookParams.builder()
.body(body)
.headers(headers.build())
.build());
event.data().sessionStatusIdled().ifPresent(idled ->
IO.println("session idled: " + idled.id()));
// tangani jenis event lainnya
exchange.sendResponseHeaders(200, -1);
} catch (Exception _) {
exchange.sendResponseHeaders(400, -1);
}
exchange.close();
});
} use Juglow\Client;
use Juglow\Core\Exceptions\WebhookException;
$client = new Client(); // reads JUGLOW_WEBHOOK_SIGNING_KEY from env
$body = file_get_contents('php://input');
$headers = getallheaders();
try {
// unwrap() melempar error jika tanda tangan tidak valid atau payload sudah kedaluwarsa
$event = $client->beta->webhooks->unwrap($body, headers: $headers);
} catch (WebhookException) {
http_response_code(400);
exit('invalid signature');
}
match ($event->data->type) {
'session.status_idled' => print "session idled: {$event->data->id}\n",
// tangani jenis event lainnya
default => null,
};
http_response_code(200); require "sinatra"
require "juglow"
client = Juglow::Client.new # reads JUGLOW_WEBHOOK_SIGNING_KEY from env
post "/webhook" do
headers = request.env
.select { |key, _| key.start_with?("HTTP_") }
.transform_keys { it.delete_prefix("HTTP_").downcase.tr("_", "-") }
begin
# unwrap memunculkan error jika tanda tangan tidak valid atau payload sudah kedaluwarsa
event = client.beta.webhooks.unwrap(request.body.read, headers: headers)
rescue StandardError
halt 400, "invalid signature"
end
if event.data.type == :"session.status_idled"
puts "session idled: #{event.data.id}"
end
# tangani jenis event lainnya
status 200
endMenangani peristiwa
Parse body, lakukan switch pada data.type, dan ambil sumber daya berdasarkan ID. Kembalikan 2xx apa pun untuk mengonfirmasi penerimaan. Respons lain apa pun dihitung sebagai kegagalan endpoint: 3xx langsung menonaktifkannya (redirect tidak pernah diikuti), sedangkan kegagalan lainnya dicoba ulang; lihat Perilaku pengiriman untuk aturan percobaan ulang dan penonaktifan otomatis.
Setiap payload peristiwa memiliki struktur yang sama, termasuk jenis peristiwa, pengenal, dan timestamp kapan peristiwa tersebut terjadi.
{
"type": "event",
"id": "whe_9d5c1f7e...",
"created_at": "2026-03-18T14:05:22Z",
"data": {
"type": "session.status_idled",
"id": "sesn_01XYZ...",
"organization_id": "8a3d2f1e-...",
"workspace_id": "c7b0e4d9-..."
}
} if event.data.type == "session.status_idled":
session = client.beta.sessions.retrieve(event.data.id)
notify_user(session)
return "", 204 if (event.data.type === "session.status_idled") {
const session = await client.beta.sessions.retrieve(event.data.id);
notifyUser(session);
}
res.sendStatus(204); if (webhookEvent.Data.TryPickSessionStatusIdled(out var idled))
{
var session = await client.Beta.Sessions.Retrieve(idled.ID);
NotifyUser(session);
}
return Results.StatusCode(204); if event.Data.Type == "session.status_idled" {
session, err := client.Beta.Sessions.Get(r.Context(), event.Data.ID, juglow.BetaSessionGetParams{})
if err != nil {
panic(err)
}
notifyUser(session)
}
w.WriteHeader(http.StatusNoContent) event.data().sessionStatusIdled().ifPresent(idled -> {
var session = client.beta().sessions().retrieve(idled.id());
notifyUser(session);
});
exchange.sendResponseHeaders(204, -1); if ($event->data->type === 'session.status_idled') {
$session = $client->beta->sessions->retrieve($event->data->id);
notifyUser($session);
}
http_response_code(204); if event.data.type == :"session.status_idled"
session = client.beta.sessions.retrieve(event.data.id)
notify_user(session)
end
status 204event.id tingkat atas bersifat unik per peristiwa, bukan per pengiriman. Jika Anda menerima event.id yang sama dua kali, itu adalah percobaan ulang dan Anda dapat membuangnya.
Perilaku pengiriman
- Duplikat: Sebuah endpoint dapat menerima peristiwa yang sama lebih dari sekali, dan setiap percobaan mengirimkan
event.idtingkat atas yang sama (nilai yang sama dengan headerwebhook-id). Lakukan deduplikasi berdasarkan nilai tersebut.
- Cakupan langganan: Sebuah peristiwa hanya dikirimkan ke endpoint yang berlangganan jenisnya pada saat peristiwa itu dipancarkan. Peristiwa yang dipancarkan saat tidak ada endpoint yang berlangganan jenisnya tidak akan pernah dikirimkan, dan berlangganan di kemudian hari tidak akan mengisinya kembali, jadi berlanggananlah ke suatu jenis peristiwa sebelum Anda membutuhkannya.
- Urutan tidak dijamin. Peristiwa tidak dikirimkan sesuai urutan terjadinya:
session.status_idledmungkin tiba sebelumsession.outcome_evaluation_endedmeskipun hasilnya diproduksi lebih dulu, dan peristiwa.deleteddapat tiba sebelum peristiwa.archiveduntuk sumber daya yang sama. Kendalikan status Anda berdasarkan sumber daya yang Anda ambil, bukan berdasarkan urutan kedatangan peristiwa.
- Percobaan ulang: Untuk setiap endpoint dan peristiwa, Juglow melakukan hingga tiga percobaan pengiriman (respons yang memicu penonaktifan otomatis, yang dijelaskan nanti di bagian ini, tidak pernah dicoba ulang) dengan exponential backoff ber-jitter antara 5 dan 120 detik. Setiap percobaan mengirimkan
event.idyang sama. Setelah percobaan terakhir gagal, peristiwa tersebut dibuang: tidak diantrekan untuk pengiriman nanti dan tidak ada sinyal bahwa peristiwa itu hilang. Webhook bukanlah log yang tahan lama, jadi jika Anda perlu mengamati setiap transisi, lakukan rekonsiliasi dengan mendaftar atau mengambil sumber daya melalui API.
- Timestamp: Header
webhook-timestampdicap saat percobaan pengiriman ditandatangani dan dibuat ulang pada setiap percobaan ulang, sehingga percobaan ulang tidak ditolak oleh pemeriksaan kesegaran SDK. Ini adalah jam untuk percobaan pengiriman, bukan untuk peristiwa: gunakancreated_atpada payload peristiwa untuk mengetahui kapan peristiwa terjadi.
- Penonaktifan otomatis: Sebuah endpoint secara otomatis diatur ke
disableddengandisabled_reasonyang dapat dibaca mesin dalam tiga kasus:
- Endpoint mengembalikan respons
3xx. Redirect tidak pernah diikuti; ini langsung menonaktifkan endpoint, pada percobaan pertama, dengan alasanauto-disabled: endpoint URL returned a redirect (3xx). Jika endpoint Anda berpindah, perbarui URL di Console dan aktifkan kembali endpoint tersebut. - URL endpoint di-resolve ke alamat IP non-publik saat Juglow terhubung. Ini langsung menonaktifkan endpoint, dengan alasan
auto-disabled: endpoint URL resolved to an invalid address. - Pengiriman ke endpoint gagal terus-menerus selama periode yang berkelanjutan, dengan alasan
auto-disabled after sustained delivery failures. Pemicunya adalah berapa lama endpoint telah gagal tanpa jeda, bukan jumlah pengiriman. Satu2xxsaja akan mengatur ulang jendela waktunya, sehingga satu peristiwa yang tidak stabil tidak dapat menonaktifkan endpoint.
Ketiganya dapat dibalikkan: aktifkan kembali endpoint di Console setelah Anda menyelesaikan masalahnya. Peristiwa yang dipancarkan saat endpoint dinonaktifkan tidak akan diputar ulang.