Dependencies & Logging
Workflows often need to interact with external services, like databases, APIs, or notification systems. flow.use() provides a mechanism for lazy-loaded dependency injection.
Signature:
flow.use<T>(binding: Binding<T>): T;
type Binding<T> = () => T;binding: (Required) A function that takes no arguments and returns an instance of the dependency (e.g., an API client, a database connection pool). This function is called the binding function.T: The type of the dependency instance returned by the binding function.
Behavior:
- The first time
flow.use()is called with a specificbindingfunction reference within a workflow instance, thebindingfunction is executed, and its return value (the dependency instance) is cached. - Subsequent calls to
flow.use()with the exact samebindingfunction reference within the same workflow instance will return the cached instance directly, without re-executing the binding function.
Why Use flow.use()?
- Lazy Loading: Dependencies are only initialized if and when they are actually needed by a workflow step, saving resources.
- Caching/Singleton: Ensures that only one instance of a dependency (per binding function) is created per workflow instance, preventing issues with multiple connections or inconsistent client states.
- Testability: Makes it easier to mock dependencies during testing by providing mock binding functions.
- Determinism: Helps maintain workflow determinism by ensuring dependencies are initialized consistently.
Important: Stable Function References
The caching mechanism relies on the reference of the binding function passed to flow.use(). To ensure caching works correctly:
- DO define your binding functions as standalone, named constants or functions, typically in a separate file (like
src/bindings/graphql.ts). - DO NOT use inline anonymous functions (
flow.use(() => new ApiClient())) inside your workflow logic, as this creates a new function reference on every call, defeating the cache.
Example:
import { defineBinding } from '@identity-flow/sdk';import { GraphQLClient } from 'graphql-request';
// Optional helper
const createClient = () => { console.log('Initializing GraphQL Client...'); // This will only log once per workflow instance return new GraphQLClient('http://localhost:4000/graphql');};
// Define the binding function using a stable const referenceexport const GraphqlBinding = defineBinding(createClient);import { defineWorkflow } from '@identity-flow/sdk';
import { GraphqlBinding } from '../bindings/graphql';
// Import the stable binding
export default defineWorkflow({ name: '@my-org/my-workflow', version: '1.0.0' }, async (flow) => { // First use: initializes the client via GraphqlBinding() const client1 = flow.use(GraphqlBinding); flow.log('Used client 1');
await flow.do('some-task', async ({ use }) => { // Can also use `use` from the task context // Second use (same instance): returns the cached client const client2 = use(GraphqlBinding); flow.log('Used client 2');
// client1 === client2 will be true const data = await client2.request('{ /* some query */ }'); return data; });});The defineBinding helper is optional but can provide better type inference in some scenarios.
Use flow.use() consistently to manage external service clients and resources within your workflows.
The flow object provides standard methods for logging messages during workflow execution. These logs are captured by the engine and associated with the workflow instance and specific step, aiding debugging.
Available methods:
flow.log(...data: any[])flow.info(...data: any[])flow.warn(...data: any[])flow.error(...data: any[])flow.debug(...data: any[])flow.trace(...data: any[])
Example:
async (flow) => { flow.info('Starting user data fetch...'); try { // ... fetch data flow.debug('User data fetched successfully', { userId: flow.params.userId }); } catch (error) { flow.error('Failed to fetch user data', error); }};With dependencies and logging covered, explore how to handle failures and ensure your workflows are robust: