Skip to content

Lambda-to-Lambda Invocation

VentureKit uses a single invoke() function for all Lambda-to-Lambda calls:

  • invoke('route/path', opts) — Call a route handler (builds API Gateway V2 event, skips API Gateway)
  • invoke({ function: 'name' }, opts) — Call a standalone function with a raw payload
import { invoke } from '@venturekit/runtime/patterns';
// Call a route handler (API GW event format)
const result = await invoke<{ id: string; name: string }>('users/{id}', {
method: 'GET',
pathParams: { id: '42' },
});
console.log(result.data); // { id: '42', name: 'Alice' }
// Call a standalone function (src/functions/process-orders.ts)
const job = await invoke<{ jobId: string }>(
{ function: 'process-orders' },
{ payload: { orderId: 'abc', items: ['widget'] } },
);
console.log(job.data.jobId);
  1. Resolves the Lambda function name from the route path (myapp-users-{id})
  2. Builds a full API Gateway V2 event payload (same as external HTTP requests)
  3. Calls Lambda directly via AWS SDK InvokeCommandno API Gateway hop
  4. Propagates trace context, tenant ID, user ID, and an x-venturekit-source: internal header
  5. Parses the response back into a typed result

Your handler receives an identical event whether called externally (via API Gateway) or internally (via invoke()). No code changes needed.

Function invocation (invoke({ function: 'name' }, ...))

Section titled “Function invocation (invoke({ function: 'name' }, ...))”
  1. Resolves the Lambda name: {project}-{stage}-fn-{name} (e.g. myapp-dev-fn-process-orders)
  2. Sends the raw payload (JSON-serialized) — no API Gateway event wrapping
  3. Returns the function’s response as typed data

You only use the short name as it appears in your project (e.g. 'process-orders'). VentureKit adds the project prefix and stage automatically.

import { invoke } from '@venturekit/runtime/patterns';
// GET with query params
const users = await invoke<User[]>('users/list', {
method: 'GET',
queryParams: { page: '1', limit: '20' },
});
// POST with body
const created = await invoke<User>('users/create', {
body: { name: 'Alice', email: 'alice@example.com' },
});
// With path parameters
const user = await invoke<User>('users/{id}', {
method: 'GET',
pathParams: { id: '42' },
});
// Fire-and-forget (async invocation)
await invoke('notifications/send', {
mode: 'async',
body: { userId: '42', message: 'Hello!' },
});
// Context (trace, tenant, user) is auto-propagated — no need to pass explicitly
await invoke('orders/create', {
body: orderData,
});
route: 'users/list' → Lambda: '{project}-users-list'
route: 'tasks/{id}' → Lambda: '{project}-tasks-{id}'

Call standalone functions in src/functions/ by their short name:

import { invoke } from '@venturekit/runtime/patterns';
// Sync: call and wait for result
const result = await invoke<{ jobId: string }>(
{ function: 'process-orders' },
{ payload: { orderId: 'abc-123', items: [...] } },
);
console.log(result.data.jobId);
// Async: fire-and-forget
await invoke(
{ function: 'send-email' },
{ payload: { to: 'user@example.com', template: 'welcome' }, mode: 'async' },
);
// With trace propagation
await invoke(
{ function: 'audit-log' },
{ payload: { action: 'order.created', data: order }, trace: ctx.trace },
);
function: 'process-orders' → Lambda: '{project}-{stage}-fn-process-orders'
function: 'order-create-order' → Lambda: '{project}-{stage}-fn-order-create-order'

The project and stage are read from VENTURE_PROJECT_NAME and VENTURE_STAGE environment variables (set automatically during deployment and local dev).

The RequestContext includes an isInternal flag that is automatically set when a request comes from another VentureKit function via invoke():

import { handler } from '@venturekit/runtime';
export const main = handler(async (body, ctx) => {
if (ctx.isInternal) {
// Called from another Lambda via invoke() — skip rate limiting, etc.
console.log('Internal invoke ID:', ctx.invokeId);
}
return { source: ctx.isInternal ? 'internal' : 'external' };
});

No need to check headers manually — ctx.isInternal and ctx.invokeId are populated automatically by buildContext().

invoke() automatically propagates context from the calling function:

  • Trace context — extracted from request headers (x-trace-id) via extractTraceContext(), or from event.__trace in task handlers
  • Tenant ID — extracted from request headers or context
  • User ID — extracted from request headers or context

You can override any of these explicitly:

await invoke('orders/create', {
body: orderData,
trace: customTrace, // override auto-propagated trace
tenantId: 'tenant-42', // override auto-propagated tenant
userId: 'user-99', // override auto-propagated user
});

invoke() works out of the box during vk dev. When VENTURE_LOCAL=true, calls are automatically routed to the local dev server via HTTP instead of the AWS Lambda SDK:

  • Route invocations become HTTP requests to http://localhost:{port}/{path}
  • Function invocations become POST http://localhost:{port}/_dev/invoke/{name}

No configuration needed — just run vk dev and invoke() works.

For full control, use the lower-level createInvoker() API:

import { createInvoker, createLambdaInvoker, defaultFunctionNameResolver } from '@venturekit/runtime/patterns';
const lambdaInvoker = await createLambdaInvoker({ region: 'eu-west-1' });
const invoker = createInvoker({
invoker: lambdaInvoker,
resolver: defaultFunctionNameResolver('my-custom-prefix'),
});
const result = await invoker('users/get');

Lambda functions need lambda:InvokeFunction permission to call other functions. VentureKit’s CDK stack grants this automatically for functions in the same stack.