---
title: Writing Resources
description: Guide to creating descriptor-backed workflows and agents with the Elevasis SDK
---

Resources are the building blocks of your Elevasis project. Resource identity and governance metadata live in the Organization Model under the id-keyed `organizationModel.resources` map; operations code imports those descriptors, attaches executable workflow or agent behavior, and exports a `DeploymentSpec` for deployment.

This page covers how to structure executable resources, derive runtime IDs from OM descriptors, write step handlers, and assemble the deployment spec without creating a second identity catalog.

## WorkflowDefinition

A workflow is a series of named steps executed in sequence or branching paths. You define it with a `WorkflowDefinition` object.

A complete workflow definition has four required properties:

- `config` - metadata about the workflow (name, description, status)
- `contract` - Zod schemas for input and output validation
- `steps` - a record of named step handlers
- `entryPoint` - the name of the first step to execute

### Minimal Example

```typescript
import { z } from 'zod';
import type { WorkflowDefinition } from '@elevasis/sdk';
import { resourceDescriptors } from '@core/config/organization-model';

const echoInput = z.object({
  message: z.string(),
});

const echoOutput = z.object({
  result: z.string(),
});

type EchoInput = z.infer<typeof echoInput>;

const echoWorkflow: WorkflowDefinition = {
  config: {
    resource: resourceDescriptors.echo,
    resourceId: resourceDescriptors.echo.id,
    name: 'Echo',
    type: resourceDescriptors.echo.kind,
    description: 'Returns the input message unchanged',
    version: '1.0.0',
    status: 'dev',
  },
  contract: {
    inputSchema: echoInput,
    outputSchema: echoOutput,
  },
  steps: {
    run: {
      id: 'run',
      name: 'Run',
      description: 'Returns the input message unchanged',
      handler: async (input) => {
        const { message } = input as EchoInput;
        return { result: message };
      },
      inputSchema: echoInput,
      outputSchema: echoOutput,
      next: null,
    },
  },
  entryPoint: 'run',
};
```

### config

The `config` block binds executable behavior to the OM resource descriptor:

| Field         | Type              | Description                                                                                                                        |
| ------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `resource`    | `ResourceEntry`   | OM descriptor from `core/config/organization-model.ts`; owns stable identity, kind, system membership, ownership, and status.      |
| `resourceId`  | `string`          | Derived from `resource.id`. Used by runtime invocation, CLI targeting, execution logs, and dashboard links. Do not invent it here. |
| `name`        | `string`          | Human-readable display name shown in the dashboard.                                                                                |
| `type`        | `'workflow'`      | Derived from `resource.kind` for workflow resources.                                                                               |
| `description` | `string`          | Summary shown in the dashboard and command center.                                                                                 |
| `version`     | `string`          | Semver string (e.g., `'1.0.0'`). Increment when making breaking changes to the contract.                                           |
| `status`      | `'dev' | 'prod'` | Controls runtime availability. Use `'dev'` while building. See [Resource Status](#resource-status).                                |
| `domains`     | `string[]`        | Optional. Groups resources in the command center view.                                                                             |

### contract

The `contract` block defines the Zod schemas for input and output. The platform validates input against `contract.inputSchema` before passing it to your entry step handler, and validates the final step's return value against `contract.outputSchema`.

Always use `z.object()` for both schemas. Use `z.infer` to derive TypeScript types -- this keeps types and runtime validation in sync automatically.

```typescript
const contract = {
  inputSchema: z.object({
    userId: z.string().uuid(),
    action: z.string(),
  }),
  outputSchema: z.object({
    success: z.boolean(),
    message: z.string(),
  }),
};
```

### steps

The `steps` record maps step IDs to step objects. Each step has metadata (`id`, `name`, `description`), a `handler` function, Zod schemas for its own input and output, and a `next` property that controls routing.

```typescript
const steps = {
  validate: {
    id: 'validate',
    name: 'Validate',
    description: 'Validates the incoming request',
    handler: async (input: MyInput) => {
      return { validated: true, data: input };
    },
    inputSchema: myInputSchema,
    outputSchema: z.object({ validated: z.boolean(), data: myInputSchema }),
    next: { type: 'linear', target: 'process' }, // points to the next step
  },
  process: {
    id: 'process',
    name: 'Process',
    description: 'Processes the validated data',
    handler: async (input: ValidatedData) => {
      return { result: 'done' };
    },
    inputSchema: z.object({ validated: z.boolean(), data: myInputSchema }),
    outputSchema: z.object({ result: z.string() }),
    next: null, // terminal step
  },
};
```

### entryPoint

`entryPoint` is the name of the first step. For single-step workflows it is always the only step name. For multi-step workflows it is the name of the first step in the chain.

```typescript
const workflow: WorkflowDefinition = {
  // ...
  entryPoint: 'validate', // execution starts here
};
```

---

## Multi-Step Workflows

For workflows with more than one step, import `StepType` from `@elevasis/sdk` to configure step routing.

### Linear Steps

Use `StepType.LINEAR` to connect steps in a fixed sequence. Each step declares a `next` property pointing to the next step name.

```typescript
import { z } from 'zod';
import type { WorkflowDefinition, WorkflowStep } from '@elevasis/sdk';

const fetchStep: WorkflowStep = {
  id: 'fetch',
  name: 'Fetch',
  description: 'Fetches the raw data',
  handler: async (input) => {
    const data = await fetchSomething((input as { id: string }).id);
    return { data };
  },
  inputSchema: z.object({ id: z.string() }),
  outputSchema: z.object({ data: z.unknown() }),
  next: { type: 'linear', target: 'process' },
};

const processStep: WorkflowStep = {
  id: 'process',
  name: 'Process',
  description: 'Transforms the fetched data',
  handler: async (input) => {
    return { result: transform((input as { data: unknown }).data) };
  },
  inputSchema: z.object({ data: z.unknown() }),
  outputSchema: z.object({ result: z.string() }),
  next: null, // terminal step
};

const pipeline: WorkflowDefinition = {
  config: {
    resourceId: 'pipeline',
    name: 'Pipeline',
    type: 'workflow',
    description: 'Fetch then process',
    version: '1.0.0',
    status: 'dev',
  },
  contract: { inputSchema: pipelineInput, outputSchema: pipelineOutput },
  steps: { fetch: fetchStep, process: processStep },
  entryPoint: 'fetch',
};
```

### Conditional Branching

Use `StepType.CONDITIONAL` to route to different steps based on output values. Each route has a `condition` function and a `target` step name. The first condition that returns `true` wins. A `default` fallback step is required.

```typescript
import { z } from 'zod';
import type { WorkflowStep } from '@elevasis/sdk';

const routerStep: WorkflowStep = {
  id: 'router',
  name: 'Router',
  description: 'Scores the input and routes to the appropriate path',
  handler: async (input) => {
    const score = await evaluate(input);
    return { score };
  },
  inputSchema: z.object({ value: z.unknown() }),
  outputSchema: z.object({ score: z.number() }),
  next: {
    type: 'conditional',
    routes: [
      {
        condition: (output) => (output as { score: number }).score >= 80,
        target: 'highScorePath',
      },
      {
        condition: (output) => (output as { score: number }).score >= 50,
        target: 'mediumScorePath',
      },
    ],
    default: 'lowScorePath',
  },
};
```

---

## StepContext

Every step handler can optionally accept a second `context` parameter. TypeScript allows you to omit it if you do not need it -- you can write `handler: async (input) => { ... }` without declaring `context`.

When you do need it, the context provides runtime metadata:

```typescript
import type { StepHandler } from '@elevasis/sdk';

const myStep: StepHandler = async (input, context) => {
  const { executionId, organizationId, resourceId, logger, store } = context;

  logger.info('Starting step', { executionId });

  // store is a simple key-value store scoped to this execution
  await store.set('progress', '50%');

  return { done: true };
};
```

| Field            | Type     | Description                                                    |
| ---------------- | -------- | -------------------------------------------------------------- |
| `executionId`    | `string` | Unique ID for this execution run                               |
| `organizationId` | `string` | Your organization ID                                           |
| `resourceId`     | `string` | ID of the workflow or agent being executed                     |
| `logger`         | `Logger` | Structured logger -- messages appear in the Elevasis dashboard |
| `store`          | `Store`  | Key-value store scoped to the current execution                |

---

## AgentDefinition

Agents are autonomous resources that use an LLM and tools to complete a goal. You define them with `AgentDefinition`.

**Note:** Use `elevasis-sdk exec --async` when executing agents. Agents can run for minutes or longer, and the synchronous execute endpoint will time out for long-running runs. The `--async` flag returns an execution ID immediately and polls for the result.

```typescript
import type { AgentDefinition } from '@elevasis/sdk';
import { resourceDescriptors } from '@core/config/organization-model';

const myAgent: AgentDefinition = {
  config: {
    resource: resourceDescriptors.myAgent,
    resourceId: resourceDescriptors.myAgent.id,
    name: 'my-agent',
    type: resourceDescriptors.myAgent.kind,
    description: 'Answers questions using platform tools',
    status: 'dev',
  },
  agentConfig: {
    model: { provider: 'openai', model: 'gpt-5' },
    systemPrompt: 'You are a helpful assistant.',
    maxIterations: 10,
  },
  tools: [],
};
```

---

## DeploymentSpec

All executable resources must be assembled through a `DeploymentSpec` default export from `operations/src/index.ts`. This is the entry point the platform reads when you deploy. Include the Organization Model payload so the validator can compare code-backed resources against OM descriptors.

```typescript
import type { DeploymentSpec } from '@elevasis/sdk';
import { organizationModel } from '@core/config/organization-model';

const org: DeploymentSpec = {
  version: '0.1.0',
  organizationModel,
  workflows: [echoWorkflow, pipeline],
  agents: [
    // myAgent,
  ],
};

export default org;
```

Each resource's `config.resourceId` is still the deployed runtime identifier. The important authoring rule is that `config.resourceId` derives from the OM descriptor (`resource.id`) instead of being duplicated by hand.

### Resource Status

Set `config.status` to control whether the platform accepts execution requests:

- `'dev'` - Resource is visible in the dashboard but the platform will not route scheduled or webhook-triggered executions to it. You can still trigger it manually via `elevasis-sdk exec`.
- `'prod'` - Resource is live and accepts all execution sources.

Use `'dev'` while you are building and testing. Switch to `'prod'` when you are ready to go live.

You can also set a global default status in `elevasis.config.ts`:

```typescript
import type { ElevasConfig } from '@elevasis/sdk';

const config: ElevasConfig = {
  defaultStatus: 'dev',
};

export default config;
```

## Documentation

- [SDK Types](types.mdx) - Complete type reference for `@elevasis/sdk` exports, config, and step handler context
- [Common Patterns](patterns.mdx) - Sequential steps, conditional branching, error handling, and resource status patterns

---

**Last Updated:** 2026-02-25
