Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions skills/velt-approval-engine-best-practices/AGENTS.full.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Velt Approval Engine Best Practices

**Version 1.0.0**
**Version 1.0.2**
Velt
May 2026

Expand Down Expand Up @@ -31,7 +31,8 @@ Velt Approval Engine implementation guide covering the declarative workflow runt
- 2.5 [Steps endpoints — recordReviewerDecision, recordAgentResolution, cancel (admin), resolve (admin)](#25-steps-endpoints-recordreviewerdecision-recordagentresolution-cancel-admin-resolve-admin)

3. [Webhooks](#3-webhooks) — **HIGH**
- 3.1 [Webhook delivery — HMAC verification on raw bytes, payload shape, event catalog with data highlights, retry schedule, idempotency on (executionId, seq)](#31-webhook-delivery-hmac-verification-on-raw-bytes-payload-shape-event-catalog-with-data-highlights-retry-schedule-idempotency-on-executionid-seq)
- 3.1 [Inbound webhook handler — raw JSON ingress with bearer auth, signed callbacks, rate/size limits, SSRF guard](#31-inbound-webhook-handler-raw-json-ingress-with-bearer-auth-signed-callbacks-ratesize-limits-ssrf-guard)
- 3.2 [Webhook delivery — HMAC verification on raw bytes, payload shape, event catalog with data highlights, retry schedule, idempotency on (executionId, seq)](#32-webhook-delivery-hmac-verification-on-raw-bytes-payload-shape-event-catalog-with-data-highlights-retry-schedule-idempotency-on-executionid-seq)

---

Expand All @@ -58,6 +59,11 @@ agent Runs an agent. Non-blocking by default (completes asynchronously with

human Requires reviewer approval. Drives via /steps/recordReviewerDecision. Parks in
"waiting" until aggregator resolves.

webhook Deferred in v1. The `webhook` type passes definition validation (so authors can
draft graphs that will rely on it later), but the runtime handler is NOT enabled —
a `webhook` node will not run today. Treat it as a forward-compatibility hook,
not a runnable surface.
```

**Agent node shape:**
Expand Down Expand Up @@ -870,9 +876,47 @@ The `action` field (REQUIRED) is the central discriminator. It determines both t

**Impact: HIGH**

Inbound webhook delivery — required HMAC-SHA256 signature verification on the raw bytes (not re-serialized JSON), the three `x-velt-*` headers, the full payload field shape, the event catalog with `data` highlights per event type (including new `loop.iteration-started` and `loop.exhausted` events), the retry schedule (immediate → +2s → +8s → +32s → +2min → +8min → DLQ), the at-least-once delivery contract with idempotency on `(executionId, seq)`, the cancellation reason vocabulary, and missed-event recovery via `/executions/getEvents?sinceSeq=N`.
The two webhook surfaces of the Approval Engine. Outbound delivery (`webhooks-delivery`) — required HMAC-SHA256 signature verification on the raw bytes (not re-serialized JSON), the three `x-velt-*` headers, the full payload field shape, the event catalog with `data` highlights per event type (including new `loop.iteration-started` and `loop.exhausted` events), the retry schedule (immediate → +2s → +8s → +32s → +2min → +8min → DLQ), the at-least-once delivery contract with idempotency on `(executionId, seq)`, the cancellation reason vocabulary, and missed-event recovery via `/executions/getEvents?sinceSeq=N`. Inbound ingress (`webhooks-inbound-handler`) — a stable HTTP endpoint external systems POST raw JSON to (no `{data:...}` envelope), with bearer-token auth, signed callback tokens, per-source rate limiting, body-size limits, and an SSRF URL guard.

### 3.1 Inbound webhook handler — raw JSON ingress with bearer auth, signed callbacks, rate/size limits, SSRF guard

**Impact: MEDIUM-HIGH (External systems POST raw JSON directly (no {data:...} envelope); skipping the bearer token or assuming the standard REST envelope makes every inbound call fail)**

In addition to outbound delivery (`webhooks-delivery`), the Approval Engine exposes an **inbound** webhook handler: an HTTP endpoint with a stable URL that external systems (Salesforce, Stripe, GitHub, etc.) POST raw JSON to in order to push events *into* the engine. It is complementary to — not a duplicate of — outbound delivery; both paths are active independently and can coexist on the same workflow.

The key shape difference from the standard REST API: inbound payloads are **not** wrapped in a `{ data: ... }` envelope. The handler accepts raw JSON bodies directly.

**Incorrect (wrapping the body in the REST `{data:...}` envelope and omitting the bearer token):**

```text
{
"data": { "event": "deal.closed", "dealId": "abc123" }
}
POST <inbound-handler-url>
Content-Type: application/json
// no Authorization header → rejected before any processing
```

**Correct (raw JSON body + bearer token):**

```json
POST <inbound-handler-url>
Content-Type: application/json
Authorization: Bearer <token>
{ "event": "deal.closed", "dealId": "abc123" }
```

The inbound handler enforces, at the boundary:
- **Bearer-token validation** — requests must carry a valid bearer token; unauthenticated requests are rejected before any processing.
- **Signed callback tokens** — callbacks issued by the handler are signed so downstream consumers can verify authenticity end-to-end.
- **Rate limiting** — per-source request rate is capped to prevent abuse.
- **Body-size limits** — oversized payloads are rejected before parsing.
- **SSRF URL guard** — any URL values in the incoming payload are validated against an allowlist to block server-side request forgery.
Keep the two surfaces straight: **inbound** = external systems push events *into* the engine (this rule); **outbound** = the engine pushes state-change events *out* to your receiver via the per-dispatch `webhookUrl` + `webhookSecret` (`webhooks-delivery`). This is also distinct from the deferred `node.type === "webhook"` step type, which validates in a definition but does not run in v1 (`concepts-workflow-model`).

---

### 3.1 Webhook delivery — HMAC verification on raw bytes, payload shape, event catalog with data highlights, retry schedule, idempotency on (executionId, seq)
### 3.2 Webhook delivery — HMAC verification on raw bytes, payload shape, event catalog with data highlights, retry schedule, idempotency on (executionId, seq)

**Impact: HIGH (Wrong signature verification (hashing re-serialized JSON instead of raw bytes) lets forged requests through; missing seq-based idempotency double-processes every retried event)**

Expand Down Expand Up @@ -1035,3 +1079,4 @@ The receiver and the reconciler must share the same dedup logic (same `(executio
- https://docs.velt.dev/api-reference/rest-apis/v2/approval-engine/definitions/create-definition
- https://docs.velt.dev/api-reference/rest-apis/v2/approval-engine/executions/dispatch-execution
- https://docs.velt.dev/api-reference/rest-apis/v2/approval-engine/steps/record-reviewer-decision
- https://docs.velt.dev/ai/approval-engine/overview#inbound-webhook-handler
4 changes: 2 additions & 2 deletions skills/velt-approval-engine-best-practices/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Velt Approval Engine Best Practices
|v1.0.0|Velt|May 2026
|v1.0.2|Velt|May 2026
|IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning for any Velt tasks.
|root: ./rules

Expand All @@ -10,4 +10,4 @@
|shared/rest:{rest-foundations.md,rest-definitions.md,rest-executions.md,rest-object-views.md,rest-steps.md}

## 3. Webhooks — HIGH
|shared/webhooks:{webhooks-delivery.md}
|shared/webhooks:{webhooks-inbound-handler.md,webhooks-delivery.md}
2 changes: 2 additions & 0 deletions skills/velt-approval-engine-best-practices/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ For general Velt REST API patterns (Comments, Notifications, Activity, etc.), se

### Webhooks (HIGH)
- `webhooks-delivery` — HMAC-SHA256 signature verification on raw bytes, delivery headers, retry schedule, event-type catalog (now includes `loop.iteration-started` and `loop.exhausted`), at-least-once idempotency
- `webhooks-inbound-handler` — inbound HTTP endpoint that external systems POST raw JSON to (no `{data: ...}` envelope), bearer-token auth, signed callback tokens, per-source rate limiting, body-size limits, SSRF URL guard; distinct from outbound delivery and from the deferred `node.type === "webhook"`

## How to Use

Expand All @@ -60,6 +61,7 @@ rules/shared/rest/rest-executions.md
rules/shared/rest/rest-steps.md
rules/shared/rest/rest-object-views.md
rules/shared/webhooks/webhooks-delivery.md
rules/shared/webhooks/webhooks-inbound-handler.md
```

Each rule file contains:
Expand Down
5 changes: 3 additions & 2 deletions skills/velt-approval-engine-best-practices/metadata.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.0.0",
"version": "1.0.2",
"organization": "Velt",
"date": "May 2026",
"abstract": "Velt Approval Engine implementation guide covering the declarative workflow runtime for multi-step agent + human approval processes — the 14 REST endpoints under /v2/workflow/*, the definition model (nodes + edges + parallel-quorum groups), execution dispatch with idempotency, HMAC-SHA256 webhook signature verification, missed-event recovery via getEvents+sinceSeq, SLA breach edges, and the quorum policies (waitAll / cancelOnQuorum / joinOnQuorum).",
Expand All @@ -10,6 +10,7 @@
"https://docs.velt.dev/ai/approval-engine/customize-behavior",
"https://docs.velt.dev/api-reference/rest-apis/v2/approval-engine/definitions/create-definition",
"https://docs.velt.dev/api-reference/rest-apis/v2/approval-engine/executions/dispatch-execution",
"https://docs.velt.dev/api-reference/rest-apis/v2/approval-engine/steps/record-reviewer-decision"
"https://docs.velt.dev/api-reference/rest-apis/v2/approval-engine/steps/record-reviewer-decision",
"https://docs.velt.dev/ai/approval-engine/overview#inbound-webhook-handler"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ The section prefix (in parentheses) is the filename prefix used to group rules.
## 3. Webhooks (webhooks)

**Impact:** HIGH
**Description:** Inbound webhook delivery — required HMAC-SHA256 signature verification on the raw bytes (not re-serialized JSON), the three `x-velt-*` headers, the full payload field shape, the event catalog with `data` highlights per event type (including new `loop.iteration-started` and `loop.exhausted` events), the retry schedule (immediate → +2s → +8s → +32s → +2min → +8min → DLQ), the at-least-once delivery contract with idempotency on `(executionId, seq)`, the cancellation reason vocabulary, and missed-event recovery via `/executions/getEvents?sinceSeq=N`.
**Description:** The two webhook surfaces of the Approval Engine. Outbound delivery (`webhooks-delivery`) — required HMAC-SHA256 signature verification on the raw bytes (not re-serialized JSON), the three `x-velt-*` headers, the full payload field shape, the event catalog with `data` highlights per event type (including new `loop.iteration-started` and `loop.exhausted` events), the retry schedule (immediate → +2s → +8s → +32s → +2min → +8min → DLQ), the at-least-once delivery contract with idempotency on `(executionId, seq)`, the cancellation reason vocabulary, and missed-event recovery via `/executions/getEvents?sinceSeq=N`. Inbound ingress (`webhooks-inbound-handler`) — a stable HTTP endpoint external systems POST raw JSON to (no `{data:...}` envelope), with bearer-token auth, signed callback tokens, per-source rate limiting, body-size limits, and an SSRF URL guard.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Approval Engine workflow model — nodes, edges, groups, quorum policies, loop regions, and step IDs
impact: HIGH
impactDescription: Every REST payload carries these shapes; misunderstanding them produces either INVALID_ARGUMENT linter failures at create time or stuck-forever executions at runtime
tags: approval-engine, workflow, definition, nodes, edges, groups, quorum, agent, human, reviewers, reviewerIds, slaMs, onQuorumMet, requiredNodeIds, stepId, loops, onReject, reviewerEmails, commentBody
tags: approval-engine, workflow, definition, nodes, edges, groups, quorum, agent, human, webhook, reviewers, reviewerIds, slaMs, onQuorumMet, requiredNodeIds, stepId, loops, onReject, reviewerEmails, commentBody, storeDbId, tenant-partitioning, __mock__, mock-agent
---

## Approval Engine workflow model — nodes, edges, groups, quorum policies, loop regions, and step IDs
Expand All @@ -20,8 +20,23 @@ agent Runs an agent. Non-blocking by default (completes asynchronously with

human Requires reviewer approval. Drives via /steps/recordReviewerDecision. Parks in
"waiting" until aggregator resolves.

webhook Deferred in v1. The `webhook` type passes definition validation (so authors can
draft graphs that will rely on it later), but the runtime handler is NOT enabled —
a `webhook` node will not run today. Treat it as a forward-compatibility hook,
not a runnable surface.
```

**`webhook` vs. inbound handler vs. outbound delivery — three distinct surfaces:**

The string `webhook` appears in three unrelated places in the Approval Engine. Do NOT conflate them:

1. **`node.type === "webhook"`** — a *deferred* node type that validates in a definition but does not run in v1 (above).
2. **Inbound webhook handler** — a *live* HTTP endpoint that external systems POST raw JSON to (covered in `webhooks-inbound-handler`).
3. **Per-execution outbound delivery** — the `webhookUrl` + `webhookSecret` pair set on dispatch that pushes externally-visible events to your receiver (covered in `webhooks-delivery`).

The inbound handler and outbound delivery are both live in v1 and active independently; the `webhook` node is not.

**Agent node shape:**

```json
Expand All @@ -42,6 +57,10 @@ human Requires reviewer approval. Drives via /steps/recordReviewerDecision.

Agent node config fields: `agentId` (required), `promptOverride` (≤ 8000 chars), `inputMapping` (object), `blocking` (default false), `resolutionPolicy` (**required when `blocking: true`**: `{ kind: "allResolved" | "minResolved", minCount?: integer }`; `minCount` is required when `kind === "minResolved"`), `agentMaxRuntimeMs` (≤ 86_400_000 / 24h), `requireNonEmptyOutput` (boolean).

**Reserved `__mock__` agent id:**

`agentId: "__mock__"` is a reserved identifier that lets you run a definition end-to-end without registering a real agent. The engine accepts the node, treats the step as terminal, and lets the workflow advance — useful for testing the graph shape, edge expressions, and webhook receiver before any real agent code exists. Swap to a real `agentId` before production; `__mock__` does not invoke any agent runtime.

**Human node shape (new — preferred):**

```json
Expand Down Expand Up @@ -279,8 +298,20 @@ Step: pending → running → (waiting) → completed | failed | skipped |
// `waiting` applies only to human steps and blocking:true agent steps
```

**Tenant partitioning — `storeDbId`:**

Approval Engine state is partitioned per tenant: each tenant's executions, definitions, and events are routed to that tenant's dedicated `storeDbId`. Two operational consequences:

- A definition created under tenant A is not visible to tenant B; an `executionId` from one tenant cannot be fetched, cancelled, or replayed under another tenant's API key.
- Recovery via `/executions/getEvents?sinceSeq=` reads from the partition that owns the execution. There is no cross-tenant event stream.

Treat the partition boundary as a hard isolation boundary when designing multi-tenant integrations — never assume an ID is portable across tenants.

**Verification Checklist:**
- [ ] Node `type` is one of `agent` / `human`
- [ ] Node `type` is one of `agent` / `human` / `webhook` (the `webhook` type validates but does NOT run in v1 — don't depend on its runtime behavior)
- [ ] Code that switches on `webhook` does not confuse the deferred node type with the live [[webhooks-inbound-handler]] or the live outbound delivery covered in [[webhooks-delivery]]
- [ ] Test runs that use `agentId: "__mock__"` are swapped to a real `agentId` before production
- [ ] Multi-tenant integrations treat `storeDbId` as a hard isolation boundary — no IDs assumed portable across tenants
- [ ] Every `human` node provides exactly one of `reviewers[]` or `reviewerIds[]` — never both
- [ ] Every `human` node using the new shape has at least one `reviewers[].mandatory: true`
- [ ] Every `human` node satisfies strict-mode: has `config.onReject` set OR is a member of a top-level `loops[]` body
Expand All @@ -297,5 +328,6 @@ Step: pending → running → (waiting) → completed | failed | skipped |
- [ ] Every `loops[]` entry: `entryNodeId` is in `bodyNodeIds`, `maxIterations` 1–20, no node appears in more than one loop body, `onExhausted.routeToNodeId` (if set) references a node outside the loop body

**Source Pointers:**
- https://docs.velt.dev/ai/approval-engine/overview — concepts overview, step ID formats
- https://docs.velt.dev/ai/approval-engine/customize-behavior — full node configuration, edge expressions, quorum policies, SLAs
- https://docs.velt.dev/ai/approval-engine/overview — concepts overview, step ID formats, deferred `webhook` node type, tenant `storeDbId` partitioning
- https://docs.velt.dev/ai/approval-engine/customize-behavior — full node configuration, edge expressions, quorum policies, SLAs, deferred `webhook` node clarification vs. inbound/outbound surfaces
- https://docs.velt.dev/ai/approval-engine/setup — reserved `__mock__` agent id for end-to-end tests
Loading