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 InvokeCommand — no 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.