Skip to content

Defining Workflows

Before you can define workflows, you need to set up your project and install the necessary SDK.

While IdentityFlow doesn’t enforce a specific structure for your source code if you’re building workflows as part of a larger application, the engine typically discovers workflow definitions from a specific location at runtime. For standard deployments, the recommended structure aligns with the engine’s default configuration:

your-project-root/
├── config/ # Engine configuration typically resides here
│ ├── workflows/ # Recommended location for workflow definitions & related files
│ │ ├── bindings/ # Bindings specific to these workflows
│ │ │ └── graphql.ts
│ │ ├── types/ # Optional custom types for these workflows
│ │ │ └── custom-types.ts
│ │ ├── simple-approval.workflow.ts
│ │ ├── user-onboarding.flow.ts
│ │ ├── package.json # Optional: If workflows have specific dependencies
│ │ ├── tsconfig.json # Optional: If workflows need specific TS config
│ │ └── ...
│ └── identity-flow.conf # Example engine configuration file
└── ... (other project files)
  • config/workflows/: Placing your workflow definition .ts files here allows the engine to discover them automatically with minimal configuration. This is the recommended location for simpler deployments. The engine will compile/bundle these TypeScript files internally.
  • Related Files: You can also co-locate related files like bindings, custom types, and even package.json or tsconfig.json within this directory if your workflows form a somewhat self-contained unit or have specific dependencies/compilation needs separate from a main application.
  • File Naming: The engine often looks for files ending in .workflow.ts, .flow.ts, etc. (configurable). Using clear names like user-onboarding.workflow.ts is good practice.

Placing workflows and their immediate dependencies in config/workflows/ often simplifies deployment as the engine is configured to look there by default.

  • Your workflow definition .ts files (like simple-flow.ts) need to be placed in a location where the IdentityFlow engine can discover them.
  • The engine compiles and bundles these TypeScript files internally when it first discovers them or when they change. You do not need a separate build step for your workflow files.
    • Internal compilation uses Sucrase for fast TypeScript transformation.
    • Support is included for modern features like dynamic imports (import('./module')), JSON imports (import data from './data.json' assert { type: 'json' }), and WASM imports (import wasm from './module.wasm' assert { type: 'wasm' }).
    • The compiled/bundled output is stored internally by the engine (typically in the database) for efficient execution.
  • A common and recommended approach for standard deployments is to place your workflow definition .ts files within a workflows subdirectory relative to your engine’s configuration file (e.g., config/workflows/).
  • The engine automatically searches for files matching patterns like **/*.workflow.ts, **/*.flow.ts, etc., within the configured path(s).
    • Default Include Patterns: By default, the engine looks for files matching globs like **/*.{request,approval,flow,workflow,wf}.?(m)[jt]s and **/workflows/*.?(m)[jt]s.
    • Default Exclude Patterns: Common patterns like .d.ts files, test files (*.test.ts, *.spec.ts), configuration files, node_modules, dist, build, etc., are excluded by default.
  • You can customize the search paths and include/exclude patterns in the engine’s configuration file.
  • Refer to the IdentityFlow Engine Deployment and Configuration Guide (link TBD) for detailed options.
  • Ensure draft: false is set in your defineWorkflow configuration for definitions ready for use.

The core package for defining workflows is @identity-flow/sdk. Install it in your project using your preferred package manager:

Terminal window
npm i @identity-flow/sdk

This package provides the defineWorkflow function and the flow helper object you’ll use extensively.


Workflows are defined in TypeScript files using the defineWorkflow function exported by @identity-flow/sdk.

This function takes two arguments:

  1. Configuration Object: An object specifying metadata and default settings for the workflow.
  2. Execute Function: An asynchronous function (async (flow) => { ... }) containing the actual logic of your workflow.

Let’s look at a minimal workflow definition:

src/workflows/simple-flow.ts
import { defineWorkflow } from '@identity-flow/sdk';
export default defineWorkflow(
{
// Core identification
name: '@carv/simple-flow', // Unique identifier for the workflow definition
version: '1.0.0', // Semantic version for this definition
// Optional metadata (useful for UI and organization)
label: 'Simple Flow', // Human-readable name
description: 'A basic example workflow',
// Deployment setting
draft: false, // Set to false to allow engine deployment
},
// The main logic of the workflow
async (flow) => {
flow.log('Workflow instance started!');
// Your workflow steps will go here...
const result = 'Hello from workflow!';
flow.log('Workflow instance finishing.');
return result; // The final result of the workflow instance
},
);

Key Configuration Properties:

  • name: (Required) A unique string identifying this workflow definition (e.g., @your-org/process-name).
  • version: (Required) A version string (preferably SemVer like 1.0.0) to track changes.
  • label: (Optional) A user-friendly name displayed in UIs.
  • description: (Optional) A brief explanation of the workflow’s purpose.
  • draft: (Optional, defaults to true) Set to false to indicate the workflow is ready for deployment/use by the engine. Draft workflows are typically ignored by deployment processes.
  • releaseChannel: (Optional, defaults to 'latest') Used to tag specific versions (e.g., 'beta', 'stable').
  • schema: (Optional) A validation schema (e.g., from Valibot) for the input params. See the Validation section later.
  • defaults: (Optional) Default options (like retry settings) for all activities in this workflow.
  • meta: (Optional) Arbitrary JSON-serializable metadata to attach to the workflow definition. Read via flow.definition.meta.

This definition exports a configuration object that the IdentityFlow engine can discover and register. The execute function is where the core logic resides, which we’ll explore next.


The second argument to defineWorkflow is the execute function. This is where you implement the steps and logic of your process.

async (flow) => {
// Your workflow logic goes here
};

The execute function receives a single argument, typically named flow, which is an object of type Flow (from @identity-flow/sdk). This object is your primary interface for interacting with the workflow engine and orchestrating activities.

Key properties and methods on the flow object include:

  • flow.params: Accesses the input parameters passed when the workflow instance was started. If a schema was provided in the definition config, params will be the validated and typed output.

    async (flow) => {
    const { userId, orderId } = flow.params; // Assuming schema validated these
    flow.log(`Processing order ${orderId} for user ${userId}`);
    };
  • flow.vars: A mutable object (Record<string, JSONValue>) used primarily to expose internal state externally (e.g., for UI display via the GraphQL API). This state is persisted when the workflow waits (e.g., during sleep, dialog, request), allowing external systems to query the current status or progress.

    async (flow) => {
    // Example: Expose progress for UI
    flow.vars.progress = { currentStep: 'Processing Payment', percentComplete: 50 };
    // ... workflow pauses ...
    // Update exposed progress
    flow.vars.progress = { currentStep: 'Order Shipped', percentComplete: 100 };
    };

    Important Distinction: While flow.vars is persisted, use regular function scope variables (const, let) for managing internal processing state between await points within a single execution run before the workflow pauses. flow.vars is best reserved for data you need to make visible outside the workflow execution itself. For robust state management across persisted steps, always rely on passing data through step inputs and outputs.

    Learn more about managing state effectively in Rule 3: Persist State Through Steps.

  • flow.instance: Provides read-only access to information about the current workflow instance (e.g., flow.instance.id, flow.instance.createdAt, flow.instance.meta).

  • flow.definition: Provides read-only access to the definition configuration this instance is running (e.g., flow.definition.name, flow.definition.version).

  • Logging Methods (flow.log, flow.info, flow.warn, flow.error, flow.debug, flow.trace): 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.

  • Activity Methods (flow.do, flow.sleep, flow.dialog, flow.request, flow.start): These are the core methods for defining the actual steps and pauses in your workflow. They are covered in detail in the Workflow Activities guide.

  • flow.use(): A method for lazy-loading and injecting dependencies (like API clients or services). Covered in the Dependencies & Logging guide.

  • flow.assert(): Utility to assert a condition is truthy, throwing a retryable error if not. Details in Error Handling & Retries.

The value returned by the execute function becomes the final result of the workflow instance when it completes successfully. This result is recorded in the instance’s event history and can be retrieved via the GraphQL API.

async (flow) => {
// ... logic ...
const finalReport = { processedItems: 10, status: 'Completed' };
return finalReport; // This object is the workflow's result
};

Ensuring data integrity is crucial for robust workflows. IdentityFlow integrates seamlessly with validation libraries that adhere to the Standard Schema specification (StandardSchemaV1), allowing you to define and enforce schemas for workflow parameters and activity results.

What is Standard Schema?

Built-in Support (Valibot):

IdentityFlow includes Valibot out-of-the-box. You do not need to install it separately. Access its functions conveniently via the SDK export:

import * as v from '@identity-flow/sdk/valibot';

Using Other Libraries (Standard Schema):

You can use any library compatible with StandardSchemaV1 (like Zod, ArkType, Effect Schema, etc.). Simply import the library as usual and pass its schema objects where needed.

Schema Locations:

You can apply schemas in several places:

  1. Workflow Definition (defineWorkflow): Validates the initial params passed when starting a workflow instance.

    import * as v from '@identity-flow/sdk/valibot';
    // Example using Zod (assuming zod is installed in the project)
    // import * as z from 'zod';
    // const zodSchema = z.object({ /* ... */ });
    export default defineWorkflow(
    {
    name: 'process-order',
    version: '1.0.0',
    // Pass any StandardSchemaV1 compatible schema:
    schema: v.object({
    // Validate flow.params using built-in Valibot
    orderId: v.string([v.uuid()]),
    customerId: v.string(),
    amount: v.number([v.minValue(0)]),
    items: v.array(v.object({ sku: v.string(), quantity: v.number() })),
    }),
    },
    async (flow) => {
    // flow.params is guaranteed to match the schema here
    const { orderId, amount } = flow.params;
    // ...
    },
    );

    If the input parameters provided when starting the workflow don’t match the schema, the instance creation will fail immediately.

  2. Activity Options (flow.do, flow.dialog, flow.request, flow.start): Validates the result returned by the activity or the data submitted externally to continue a dialog or request.

    // Inside execute function:
    // Validate the result of a flow.do task
    const userInfo = await flow.do(
    'fetch-user-profile',
    { schema: v.object({ name: v.string(), email: v.string([v.email()]) }) },
    async ({ use }) => {
    const apiClient = use(ApiClientBinding); // Assuming ApiClientBinding is defined
    return await apiClient.getUserProfile(flow.params.userId);
    },
    );
    // userInfo is guaranteed to match the schema here
    // Validate the data submitted to continue a dialog
    const approval = await flow.dialog(
    'manager-approval',
    { schema: v.object({ approved: v.boolean(), comments: v.optional(v.string()) }) },
    ({ token }) => ({
    params: {
    /* ... */
    },
    }),
    );
    // approval is guaranteed to match the schema here

    If the data returned by the activity or submitted via continue doesn’t match the schema provided in the options, the step will fail and potentially retry.

Benefits:

  • Type Safety: Ensures data conforms to expected structures throughout the workflow.
  • Early Error Detection: Catches invalid data close to the source.
  • Clear Contracts: Defines clear expectations for inputs and outputs of workflows and steps.

Leverage schema validation using your preferred Standard Schema library to build more reliable and predictable workflows.