Nembl
Developer Guide
External Agent Contract

External Agent Contract

Audience: developers building an external agent that plugs into a Nembl workflow as a first-class participant.

External agents register via Admin → External Agent Connections, then get attached to an Agent with the External backend (overview). When a workflow phase dispatches the agent, Nembl calls your endpoint with the envelope below, waits for your reply, and turns your AgentResult into a workflow-side Recommendation.

Two transports are supported: WEBHOOK (HTTPS POST) and MCP (Model Context Protocol over HTTPS). A2A is reserved for a future release.


1. Lifecycle (what your endpoint sees)

┌─ Nembl proxy ─────────────────────────────────────────────────────┐
│ 1. Receives the dispatch envelope from the workflow coordinator   │
│ 2. Fetches your endpoint's auth secret from your Nembl vault      │
│ 3. Signs the request per the endpoint's authType                  │
│ 4. POSTs (webhook) or invokes a tool (MCP) on your endpoint       │
│ 5. Awaits your response — timeout = endpoint.timeoutMs (def. 60s) │
│ 6. Maps your AgentResult → workflow-side Recommendation           │
└───────────────────────────────────────────────────────────────────┘

Your endpoint is synchronous within the timeout window. There is no callback URL — you respond on the request that was sent. Cancellation is communicated by Nembl closing the HTTP connection; honour AbortController semantics if your runtime supports them.


2. Webhook transport

Request

POST <your endpoint URL>
Content-Type: application/json
Nembl-Invocation-Id: <invocationId>
Nembl-Timestamp: <unix-seconds>
<auth header per authType — see §4>

Request body (ExternalDispatchEnvelope):

{
  "invocationId": "ckxxx...",      // Stable PK; idempotency key
  "agentId": "agt_...",            // The Nembl Agent dispatching to you
  "companyId": "co_...",           // Tenant scope
  "instanceId": "wfi_...",         // Workflow instance
  "phaseId": "wfp_...",            // Workflow phase
  "workflowId": "wf_...",
  "autonomyLevel": "suggest" | "act_with_approval" | "fully_autonomous",
  "variables": { /* workflow instance variables at dispatch */ },
  "capabilities": { /* the capabilities you declared at registration */ },
  "assignmentConfig": null | { /* RACI engagement config — read-only */ }
}

invocationId is stable across retries — use it as your idempotency key. No internal IDs or tokens cross this boundary.

Response

Return HTTP 200 OK with an AgentResult body:

{
  "analysis": "Short human-readable summary of what the agent decided.",
  "reasoning": "Optional longer rationale.",
  "proposedActions": [
    {
      "type": "add_comment" | "create_task" | "update_variables" | ...,
      "payload": { /* per-action shape — see §5 */ }
    }
  ],
  "tokenCount": 1234,      // Optional usage telemetry
  "model": "gpt-4o-mini",  // Optional
  "provider": "openai"     // Optional
}

Allowed type values in proposedActions are restricted to the capabilities.actions allowlist you declared at registration. Actions outside the allowlist are silently dropped and surfaced as a warning in the Nembl admin UI.

Error response

Two paths — pick whichever fits your runtime:

(a) Return 200 OK with an error envelope:

{
  "errorCode": "EXTERNAL_PROVIDER_ERROR",
  "errorMessage": "Upstream LLM returned 503",
}

(b) Return an HTTP error. Nembl maps by status code:

Your HTTP statusNembl errorCode
401 / 403EXTERNAL_AUTH_FAILED (notifies your customer's admins)
4xx (other)EXTERNAL_PROVIDER_ERROR
5xxEXTERNAL_PROVIDER_ERROR
No response within timeoutMsEXTERNAL_TIMEOUT
Non-JSON or malformed bodyEXTERNAL_INVALID_RESPONSE

3. MCP transport

Your endpoint is an MCP server speaking over HTTPS (Streamable HTTP transport). Nembl's proxy acts as an MCP client:

  • HTTPS transport only (no stdio — Nembl can't launch local processes on your side).
  • Single-tool dispatch per invocation — the connection's mcpToolName declares which tool to call.
  • Auth: BEARER or API_KEY_HEADER (HMAC is webhook-only).

Flow

  1. Nembl opens an MCP session to your endpoint URL.
  2. Lists tools (tools/list); cached in-Lambda for 5 minutes per endpoint.
  3. Calls the configured tool (tools/call) with { envelope: <envelope> } as the argument.
  4. Maps your tool result back to AgentResult.

Tool result mapping

  • If structuredContent is present and shaped like AgentResult → used directly. Recommended path.
  • If structuredContent.proposedActions: [...] is present → used for actions; analysis falls back to joined content text blocks.
  • If only content text blocks → joined into AgentResult.analysis, zero proposed actions.
  • isError: trueEXTERNAL_PROVIDER_ERROR.

Recommendation: return structuredContent matching the AgentResult shape from §2.


4. Auth payloads

authType is declared per endpoint at registration and stored in a Nembl vault you link to the endpoint. Nembl fetches the secret value at runtime — secret values never appear in CloudTrail or EventBridge events.

BEARER

Vault keys: { "bearer_token": "<token>" }

Authorization: Bearer <bearer_token>

API_KEY_HEADER

Vault keys: { "header_name": "X-Api-Key", "header_value": "<key>" }

<header_name>: <header_value>

HMAC (webhook only, recommended)

Vault keys: { "signing_secret": "<shared-secret>" }

Nembl-Timestamp: <unix-seconds>
X-Nembl-Signature: t=<unix-seconds>, v1=<hex>

Signature computation:

signed_payload = <unix-seconds> + "." + <raw-request-body>
signature      = hex( hmac_sha256(signing_secret, signed_payload) )

Verification on your side:

  1. Compute the same signature using your stored signing_secret.
  2. Compare with constant-time equality.
  3. Reject if |now - unix-seconds| > 300 (5-minute replay window).

Working TypeScript verifier:

import { createHmac, timingSafeEqual } from "node:crypto";
 
function verifyNemblSignature(opts: {
  signingSecret: string;
  rawBody: string;
  headers: Record<string, string>;
}): boolean {
  const header = opts.headers["x-nembl-signature"] ?? "";
  const match = /t=(\d+),\s*v1=([0-9a-f]+)/.exec(header);
  if (!match) return false;
  const [, ts, sig] = match;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = createHmac("sha256", opts.signingSecret)
    .update(`${ts}.${opts.rawBody}`)
    .digest("hex");
  if (expected.length !== sig.length) return false;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

NONE

No auth. Suitable only for endpoints already protected at the network layer (e.g. private mesh peering). Avoid unless you have a specific reason.


5. Action payload shapes

proposedActions[*].payload matches Nembl's workflow-action vocabulary:

TypePayload shape
add_comment{ "body": "<markdown>", "visibility": "internal" | "customer" }
create_task{ "title": "...", "description": "...", "assigneeUserId": "..." }
update_variables{ "<varName>": <value>, ... }
request_approval{ "approverUserId": "...", "reason": "..." }
escalate{ "reason": "...", "level": "team" | "manager" }
advance_phase{ "toPhaseId": "..." }

Proposed actions flow through Nembl's autonomy gates — autonomyLevel: "suggest" always renders as a human-acceptable Recommendation; act_with_approval requires a single accept click; fully_autonomous applies immediately. Your endpoint never bypasses autonomy.


6. Idempotency and retries

  • invocationId is the idempotency key. Nembl does not retry external dispatches; a single network failure surfaces as EXTERNAL_PROVIDER_ERROR.
  • Write idempotent handlers anyway — keyed on invocationId.
  • Cancellation: if a human cancels mid-flight, Nembl closes the HTTP connection. Honour AbortSignal semantics if your runtime supports them.

7. Health checks

Every 15 minutes Nembl probes your endpoint:

  • Webhook: POST <url> with body { "type": "nembl.healthcheck" } and the configured auth headers. Respond 200 with body { "ok": true }.
  • MCP: Standard MCP initialize handshake; success counts as healthy.

Three consecutive failures flip the endpoint to FAILED_HEALTH_CHECK and notify your customer's Nembl admins. First success after failure flips it back to ACTIVE.


8. Error codes (Nembl-side)

CodeWhenCustomer notification
EXTERNAL_AUTH_FAILEDYour endpoint returned 401/403Yes — customer admins
EXTERNAL_PROVIDER_ERROROther 4xx/5xx or network failureNo (logged)
EXTERNAL_INVALID_RESPONSECouldn't parse AgentResult from responseNo (logged)
EXTERNAL_TIMEOUTNo response within endpoint.timeoutMsNo (logged)
EXTERNAL_ENDPOINT_INACTIVEPre-dispatch — endpoint status was not ACTIVENo
EXTERNAL_HEALTH_CHECK_FAILEDPre-dispatch — health-check cron flipped statusYes — customer admins
PLAN_LIMIT_EXCEEDEDPre-dispatch — customer's plan doesn't include EXTERNAL dispatchesYes — customer admins, one-time

These appear in the Nembl admin UI under the agent's recent invocations, and in the workflow audit log.


9. Working reference receivers

Two minimal reference implementations live in the Nembl repo — clone and run them locally for testing:


10. Capabilities declaration

Your endpoint declares its capabilities at registration time via the "Capabilities" multi-select. This is an allowlist — proposed actions outside the declared set are dropped server-side. Declared capabilities also flow into the dispatch envelope as capabilities so your handler can branch on them.

Available actions today:

add_comment, create_task, assign_task, update_task_status,
update_variables, request_approval, escalate, advance_phase

Declare only what your endpoint actually does. Future action additions will land here without breaking existing endpoints.