Hi Jason, thanks a lot for the approach. It looks like it worked 😄! I had another quick follow up and ask you if somehow it is possible to mark the span LangGraph node span status as "ERROR"? I tried setting the status using the custom SpanProcessor onEnd() method but looks like it is being overriden/locked. Is there a workaround for this? Thanks!
class JudgeSpanCaptureProcessor implements SpanProcessor {
// keyed by "traceId:spanName" for concurrent-request safety
private readonly capturedSpanIds = new Map<string, string>();
private readonly pendingErrors = new Map<string, string>();
onStart(_span: Span, _parentContext: Context): void {}
onEnd(span: ReadableSpan): void {
const key = `${span.spanContext().traceId}:${span.name}`;
this.capturedSpanIds.set(key, span.spanContext().spanId);
// Override status BEFORE BatchSpanProcessor exports it.
// LangChain's handleChainEnd sets OK after our node runs — we fix it here.
const errorMessage = this.pendingErrors.get(key);
if (errorMessage !== undefined) {
(span as unknown as Span).setStatus({ code: SpanStatusCode.ERROR, message: errorMessage });
this.pendingErrors.delete(key);
}
}
getSpanId(traceId: string, spanName: string): string | null {
return this.capturedSpanIds.get(`${traceId}:${spanName}`) ?? null;
}
markSpanForError(traceId: string, spanName: string, message: string): void {
this.pendingErrors.set(`${traceId}:${spanName}`, message);
}
clearSpan(traceId: string, spanName: string): void {
this.capturedSpanIds.delete(`${traceId}:${spanName}`);
}
forceFlush(): Promise<void> { return Promise.resolve(); }
shutdown(): Promise<void> { return Promise.resolve(); }
}Hi, in the light of my previous question, I got the annotation working by calling forceFlush() before the mutation, thanks! But I was wondering if there is a way to annotate a specific LangGraph node from the trace instead of creating a custom span? I tried using trace.getActiveSpan() inside LangGraph node to fetch the judge node created by it but its not accessible. I tried implementing a BaseCallbackHandler attached to the graph invocation which filters by node name and tries to capture the span inside the callback context but that didnt work. Is there a better approach to annotate the LangChainInstrumentation node or capture its id? Appreciate any help I can get, thanks! 🙂
Yes that worked
Hey! I have a LangGraph-based AI workflow and using the batchUpdateAnnotations mutation to automatically label spans when our LLM judge node fails (e.g. timeout, parse error). We create a custom OTel span via startActiveSpan, extract the span ID, and fire the annotation after span.end() using BatchSpanProcessor. The mutation consistently returns BatchUpdateAnnotationSuccess: true with the correct spanId and label, and the span itself is visible in the trace UI but the Annotations tab for that span is always empty. Here is the exact payload being sent:
{
"input": {
"modelId": "<base64-encoded project ID>",
"recordAnnotationUpdates": [
{
"recordId": "207c5e526c68c0de",
"startTime": "2026-03-27T00:00:00.000Z",
"annotationUpdates": [
{
"annotationConfigId": "<our config ID>",
"annotation": {
"name": "Judge Failure Annotation",
"label": "llm_error",
"annotationType": "Label"
}
}
],
"note": { "text": "..." }
}
]
}
}Would appreciate any help I can get. Thanks! 😄
