Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions index.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
},
{
"name": "volcano-durable",
"description": "Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, and durable function schedulers.",
"description": "Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, human approvals, polling, durable executions, idempotent starts, and durable function schedulers.",
"path": "/skills/volcano-durable/SKILL.md"
},
{
Expand Down Expand Up @@ -73,7 +73,7 @@
},
{
"name": "volcano-typescript",
"description": "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime and Durable types, Function invocation generics, OAuth providers, middleware types, and utility types.",
"description": "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime, Durable, and durable approval types, Function invocation generics, OAuth providers, middleware types, and utility types.",
"path": "/skills/volcano-typescript/SKILL.md"
}
]
Expand Down
9 changes: 9 additions & 0 deletions tests/skill-trigger-cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,15 @@
{ "skill": "volcano-durable", "signals": ["long-running", "workflow", "checkpointed", "waits", "Durable Functions"] }
]
},
{
"id": "durable-human-approval",
"prompt": "Build a refund workflow that pauses for a human approval before paying out using Volcano",
"expected": [
{ "skill": "volcano-sdk", "signals": ["Volcano"] },
{ "skill": "volcano-platform", "signals": ["Volcano"] },
{ "skill": "volcano-durable", "signals": ["human approval", "workflow"] }
]
},
{
"id": "project-log-reader",
"prompt": "Build a server-side Volcano project logs reader with retained log search and pagination",
Expand Down
119 changes: 115 additions & 4 deletions volcano-durable/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: volcano-durable
description: Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, and durable function schedulers.
description: Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, human approvals, polling, durable executions, idempotent starts, and durable function schedulers.
---
# Volcano Durable Functions Skill

Expand Down Expand Up @@ -124,6 +124,7 @@ Python durable operations are synchronous. A step function receives its scope.
| `ctx.step(name?, fn, options?)` | Run work and record its result. Retry policy and at-most-once behavior belong here. |
| `ctx.wait(name?, duration)` | Suspend for at least one second without holding compute. |
| `ctx.waitUntil(name?, check, options)` | Poll state until `options.until` passes. `initialState` is required. |
| `ctx.waitForApproval(name, options)` | Suspend until a person approves or denies, or the approval expires. Python: `ctx.wait_for_approval`. |
Comment thread
subnetmarco marked this conversation as resolved.
Outdated
| `ctx.map(name?, items, fn, options?)` | Run one checkpointed child context per item. Set `concurrency` when required. |
| `ctx.parallel(name?, branches, options?)` | Run independent checkpointed branches. |
| `ctx.child(name?, fn)` | Group operations in a child context. |
Expand All @@ -134,6 +135,107 @@ durable operation. `durable get` shows the function's execution timeout and
result retention. Read the plan limits documentation for operation allowance,
operations per execution, and concurrency limits.

## Human approvals

Use `waitForApproval` when a person must sign off before the workflow continues.
The execution suspends without holding compute and resumes with the decision.
Use `ctx.waitUntil` only for state your code can read, such as a payment
settling. Do not build your own approval table and poll it.

```js
const { durable } = require('@volcano.dev/sdk/durable');

exports.handler = durable(async (input, ctx) => {
const quote = await ctx.step('quote', () => quoteShipping(input.order_id));

const decision = await ctx.waitForApproval('ship-order', {
title: `Ship order ${input.order_id}?`,
description: 'Express shipping is billed to the customer.',
details: { order_id: input.order_id, cost: quote.cost },
timeout: '3d',
});
if (!decision.approved) {
return { shipped: false, status: decision.status, comment: decision.comment };
}

await ctx.step('ship', () => ship(input.order_id));
return { shipped: true, approved_by: decision.decidedBy?.email ?? null };
});
```

```python
from volcano_sdk.durable_authoring import durable


@durable
def handler(event, ctx):
order_id = event["order_id"]
quote = ctx.step("quote", lambda scope: quote_shipping(order_id))

decision = ctx.wait_for_approval(
"ship-order",
title=f"Ship order {order_id}?",
details={"order_id": order_id, "cost": quote["cost"]},
timeout="3d",
)
if not decision.approved:
return {"shipped": False, "status": decision.status}

ctx.step("ship", lambda scope: ship(order_id))
return {"shipped": True}
```

The decision has `approved`, `status`, `comment`, `decidedBy` (`{ id, email }`
or `null`), and `decidedAt`. Python uses attributes: `decision.decided_by`
(with `id` and `email`, or `None`) and `decision.decided_at`.

| `status` | `approved` | Meaning |
|---|---|---|
| `approved` | `true` | A person approved, with an optional comment. |
| `denied` | `false` | A person denied, with an optional comment. |
| `expired` | `false` | `timeout` passed first. `comment` is empty; `decidedBy` and `decidedAt` are null. |

A deny or an expiry is a return value, not an error. Branch on
`decision.approved` and handle `expired` explicitly. An approval whose execution
ends first (stopped, failed, or timed out) shows as `cancelled` to the owner;
the workflow never receives it.

- `timeout` takes the same durations as `ctx.wait`. Without one, the approval
lasts until the execution's own timeout.
- Give each approval a stable name; it labels the operation in the execution's
history. Replay returns the recorded decision and never requests the approval
twice.
- Approvals work inside `ctx.parallel`, `ctx.map`, and child contexts. An
Comment thread
subnetmarco marked this conversation as resolved.
execution can have at most 100 pending at once; the next request throws.
- Build `title` and `details` from input or step results. `title` holds up to
200 characters, `description` 4000, and the whole request 64 KiB.
- `details` is shown to the person deciding and kept for a year. Do not put
secrets or credentials in it.

### Who decides

A person decides, in the dashboard under **Approvals**, or from the CLI after
Comment thread
subnetmarco marked this conversation as resolved.
Outdated
`volcano login`:

```sh
volcano durable approvals list # pending, newest first
volcano durable approvals get <approval-id>
volcano durable approvals approve <approval-id> --comment "Checked stock"
volcano durable approvals deny <approval-id> --comment "Customer cancelled"
volcano durable approvals stats --since 30d
```

Use `volcano durable approvals ...` locally and `volcano cloud durable
approvals ...` in cloud. The SDK owner clients expose the same operations under
`durable.approvals`. Project access tokens can read approvals and stats but get
`403` on approve and deny. The MCP tools `list_durable_approvals` and
`get_durable_approval_stats` only read; MCP has no way to decide.

Never approve or deny an approval yourself, including one your own workflow
requested during testing. List it, tell the user its ID and title, and let them
decide. Do not write code that decides approvals automatically; that defeats
the approval.

## Start and manage executions from an application

`start` accepts an application credential and returns an execution handle. An
Expand Down Expand Up @@ -250,9 +352,8 @@ finish quickly while still suspending and replaying checkpoints. Instant waits
cause more resumes per wall-clock minute than production, which helps expose a
step that is unsafe to replay. Set `LOCAL_DURABLE_REAL_TIME=true` before
`volcano start` when wait timing must be real. Local executions persist across
`volcano stop` and `volcano start`. Volcano does not expose externally completed
callbacks in local or cloud execution; use `ctx.waitUntil` to poll application
state instead.
`volcano stop` and `volcano start`. Approval timeouts always run in real time,
so test expiry locally with a short `timeout`.

Local executions increment the same execution, operation, and compute counters
as cloud executions. Inspect them through `GET /projects/{id}/usage`; the CLI
Expand Down Expand Up @@ -323,6 +424,8 @@ start.
`executions get` until terminal.
- Deleting a durable function removes its execution history. Confirm before
`durable delete`, `executions stop`, or `schedulers delete`.
- `approvals list`, `get`, and `stats` are safe to run. `approvals approve` and
`deny` are for the user alone; never run them.
- Bound `logs --follow` with a timeout in agent-driven diagnostics.

## Troubleshooting
Expand All @@ -337,6 +440,12 @@ cloud state.
5. A start during provisioning returns `409`; wait for `active`.
6. A `429` means a concurrency or durable allowance limit blocked the start.
7. A scheduler `403` can mean the project plan does not include schedulers.
8. An execution that stays `running` may be waiting on an approval. Check
`durable approvals list --execution <execution-id>`.
9. An approve or deny `403` means the credential is not a person's. A `409`
means the approval was already decided, expired, or cancelled.
10. `VOLCANO_PLATFORM_API_URL` is reserved. The platform sets it on durable
functions; a project variable with that name is rejected.

## Verification

Expand All @@ -345,6 +454,7 @@ cloud state.
- Run `volcano start` and `volcano durable deploy --all`.
- Wait for local function status `active`.
- Start one local execution with a unique idempotency name.
- If it waits on an approval, give the user its ID and ask them to decide.
- Poll it to a terminal status and check its result.
- Read runtime logs if the result is not `succeeded`.
- After approved cloud deployment, repeat the execution check with
Expand All @@ -353,6 +463,7 @@ cloud state.
## References

- Hosting contract: `volcano-hosting/docs/public/functions/durable-functions.md`
- Approvals: `volcano-hosting/docs/public/functions/durable-approvals.md`
- Local guide: `volcano-hosting/docs/public/guides/durable-functions-locally.md`
- CLI contract: `volcano-cli/docs/durable-functions.md`
- Command source: `volcano-cli/internal/cmd/durable/`
2 changes: 1 addition & 1 deletion volcano-sdk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Do NOT implement custom alternatives — no custom JWT auth, no ad-hoc database
| User accounts or identity, email or password sign-up/sign-in, OAuth, sessions, anonymous users, password recovery, private or per-user data | `volcano-auth` | Auth application flows, session lifecycle, and common-error catalog |
| Stored or persistent data, CRUD, records, todos, chat messages, polls, analytics, counters, click tracking, CMS content, feature flags, leaderboards, RLS | `volcano-database` | Query builder + every operator + RLS pattern + limitations (no joins / upserts / multi-statement tx) |
| Volcano Functions, server-side or privileged logic, QR/PDF generators, secrets, outbound third-party APIs, orchestration, scheduled processing, file/image processing | `volcano-functions` | Invocation contract `{data, status, headers, version, error}`, Volcano Functions response shape, handler templates |
| Durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, execution schedulers | `volcano-durable` | Durable authoring contract, replay rules, cloud CLI lifecycle, execution status, schedulers, and safety |
| Durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, execution schedulers, human approvals or sign-off | `volcano-durable` | Durable authoring contract, replay rules, `waitForApproval`, cloud CLI lifecycle, execution status, schedulers, and safety |
| Project logs, retained log search, pagination, activity buckets, structured log filters | `volcano-logs` | Project-token authentication, search cursors, filters, and activity counts |
| Project locks, distributed leases, leader election, fencing tokens, backend worker coordination | `volcano-locks` | Renewable lock guards, direct lease control, fencing, and safe recovery |
| Uploads, downloads, galleries, file sharing, buckets, paths, public/private files, visibility, resumable uploads | `volcano-storage` | Full storage API + access policies + resumable protocol + limits |
Expand Down
30 changes: 28 additions & 2 deletions volcano-typescript/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: volcano-typescript
description: "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime and Durable types, Function invocation generics, OAuth providers, middleware types, and utility types."
description: "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime, Durable, and durable approval types, Function invocation generics, OAuth providers, middleware types, and utility types."
---
# Volcano TypeScript Types Skill

Expand Down Expand Up @@ -378,6 +378,32 @@ const execution: DurableExecution | undefined = data?.data[0];
`DurableExecution` uses the API's snake-case fields. Its `result` is `unknown`,
so narrow it before use.

`ctx.waitForApproval` takes `WaitForApprovalOptions` and resolves with an
`ApprovalDecision`, both from the durable entry point:

```ts
import type { ApprovalDecision, WaitForApprovalOptions } from '@volcano.dev/sdk/durable';

const request: WaitForApprovalOptions = { title: 'Ship order 4417?', timeout: '3d' };
const decision: ApprovalDecision = await ctx.waitForApproval('ship-order', request);
// decision.status is 'approved' | 'denied' | 'expired'; decidedBy is null on expiry.
```

Reading and deciding approvals uses the main entry point's `DurableApproval`,
`DurableApprovalStatus`, `PaginatedDurableApprovals`, and `DurableApprovalStats`,
with `DurableApprovalListOptions` and `DurableApprovalStatsOptions` for filters:

```ts
import type { DurableApproval, DurableApprovalListOptions } from '@volcano.dev/sdk';

const options: DurableApprovalListOptions = { status: 'pending', function: 'order-pipeline' };
const { data } = await volcano.durable.approvals.list(projectId, options);
const approval: DurableApproval | undefined = data?.data[0];
```

Like `DurableExecution`, `DurableApproval` uses snake-case fields
(`requested_at`, `expires_at`, `decision.decided_by`).

## OAuth Types
```ts
type OAuthProviderName = 'google' | 'github' | 'microsoft' | 'apple';
Expand Down Expand Up @@ -567,7 +593,7 @@ channel.onPostgresChanges('INSERT', 'public', 'posts', (change) => {
| Storage | `StorageObject`, `StorageUploadResponse`, `StorageListResponse` | `storage.from(...)` operations |
| Realtime | `PostgresChange`, `PresenceState`, `ConnectContext` | Channel callbacks |
| Functions | `invoke<P, R>(...)` generic params | Both ends of an invocation |
| Durable | `DurableHandler`, `DurableContext`, `DurableExecution` | Durable authoring and execution clients |
| Durable | `DurableHandler`, `DurableContext`, `DurableExecution`, `ApprovalDecision`, `DurableApproval` | Durable authoring, approvals, and execution clients |
| OAuth | `OAuthProviderName`, `OAuthProvider` | Provider name validation |
| Sessions | `AuthSession`, `SessionsResponse` | Multi-device session UI |
| Middleware | `ServerClient`, `GetUserResult` | Next.js middleware/route handlers |
Expand Down
Loading