---
title: "Add a custom workflow"
description: "Build a resumable CLI workflow with prerequisites, polling, and human handoffs, then preserve it through regeneration."
url: https://typeship.dev/docs/guides/custom-workflows
markdown: https://typeship.dev/docs/guides/custom-workflows.md
section: "Targets"
---
> ## Documentation index
> Fetch the documentation index at https://typeship.dev/llms.txt or every prose page and both generated references at https://typeship.dev/llms-full.txt.
> Append .md to any prose docs URL, or send Accept: text/markdown, for the markdown twin of that page.
> Select existing code examples with Accept-Code-Language: typescript, python, or go (one value), or add ?codeLanguage=python to a Markdown URL. The query parameter takes precedence. Unsupported values return the full docs with a notice.

# Add a custom workflow

Build a resumable CLI workflow with prerequisites, polling, and human handoffs, then preserve it through regeneration.

A custom workflow combines generated API calls into a task your customers recognize. You own its business rules and recovery behavior. Typeship supplies the generated client and, for linked repository Deliveries, preserves non-overlapping package edits through regeneration.

This example adds `parcel-label` to a generated CLI for **Parcel, a fictional delivery service**. It checks a shipment's address, requests a label, and polls until the label is ready. The included local mock lets you run it without an account or a live shipping service.

## Choose how to compose the task

| Approach                                          | Use it when                                                                      | Authentication and state                                                                                            |
| ------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| [Wrap the generated CLI](https://typeship.dev/docs/guides/custom-cli) | Extend the existing command experience and let customers log in once.            | Subprocess calls reuse saved profiles, refresh, and generated error handling. Your wrapper owns the workflow state. |
| Call the generated client directly, as below      | Compose typed calls with control over deadlines, cancellation, and resume state. | Your code supplies credentials. This recipe reads environment variables and does not load saved CLI profiles.       |

Prefer the wrapper approach when sharing the existing login is a requirement. Both approaches can implement multi-step workflows; direct client access does not automatically integrate with the generated CLI's credential store.

## Generate and add the workflow

You need Node.js 22 or later, npm, and `curl`. Generate into a new directory:

```bash
npx -y @typeship-ax/cli@latest generate run \
  --definition '{"url":"https://typeship.dev/examples/parcel-workflow/openapi.yaml"}' \
  --target '{"generator":"cli"}' \
  --config '{"cli":{"command_name":"parcel"}}' \
  --out ./parcel-cli
cd parcel-cli
```

The [example Definition](https://typeship.dev/examples/parcel-workflow/openapi.yaml) declares three API operations. Its `api.parcel.example` server is fictional. Download the customer-authored workflow, command, tests, and mock into the generated package:

```bash
mkdir -p src tests
curl --fail -sS https://typeship.dev/examples/parcel-workflow/label-workflow.ts.txt -o src/label-workflow.ts
curl --fail -sS https://typeship.dev/examples/parcel-workflow/label-cli.ts.txt -o src/label-cli.ts
curl --fail -sS https://typeship.dev/examples/parcel-workflow/label-workflow.test.mjs -o tests/label-workflow.test.mjs
curl --fail -sS https://typeship.dev/examples/parcel-workflow/mock-server.mjs -o mock-server.mjs
```

Add the command and test script while retaining the existing `parcel` command and package scripts:

```bash title="Register the workflow"
npm pkg set 'bin.parcel-label=dist/label-cli.js' \
  'scripts.test:workflow=node --test tests/label-workflow.test.mjs'
```

```bash
npm install
npm run build
npm run test:workflow
node dist/label-cli.js --help
```

The tests exercise completion, prerequisites, handoffs, timeouts, cancellation, failure recovery, and stable creation keys. The command uses the generated client inside this CLI package; it does not require a separately generated SDK. Both compiled workflow files are included by the package's existing `dist` packaging rule.

## Run the complete task locally

In one terminal, start the mock from the package directory:

```bash
node mock-server.mjs
```

In a second terminal, enter the same directory and run:

```bash
export PARCEL_BASE_URL=http://127.0.0.1:4318
export PARCEL_TOKEN=example-token
node dist/label-cli.js start shp_123 label-for-shp_123
```

After about two seconds, expect:

```json
{"status":"ready","label_id":"lbl_1","download_url":"https://files.parcel.example/label.pdf"}
```

The download URL is illustrative; the mock creates no real label or file. It stores labels and idempotency keys in memory, so restarting it clears them. Set `PARCEL_MOCK_PORT` before starting the mock if port 4318 is occupied, and use the printed URL for `PARCEL_BASE_URL`.

The custom command reads `PARCEL_TOKEN` and `PARCEL_BASE_URL` explicitly. It does not load the generated CLI's saved login profiles. A published package installs both `parcel` and `parcel-label`; before publishing, use the `node dist/...` commands above.

## Handle each outcome

The workflow's steps are explicit:

1. Read the shipment. An unverified address returns a review URL before any label is requested.
2. Request one label with the caller's idempotency key.
3. Poll its ID once per second, with a 30-second deadline across the workflow. The deadline and Ctrl-C also cancel an active request.
4. Return the terminal state, a browser handoff, or a resumable label ID.

| Result            | Exit code  | Your next step                                                                                                                                                   |
| ----------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ready`           | `0`        | Use `download_url`. The command does not download it.                                                                                                            |
| `action_required` | `2`        | Review `message` and `action_url`. After completing the browser step, resume a returned label ID; an address handoff requires starting again with the same key.  |
| `pending`         | `2`        | Save `label_id`. Resume when ready; `reason` distinguishes timeout from cancellation.                                                                            |
| `failed`          | `1`        | Inspect the provider's message. Starting a replacement is a new business decision.                                                                               |
| Error envelope    | `1` or `2` | Read `issues[].code` and `next_steps`. Workflow context lives under `detail`, including `stage`, `label_id`, `idempotency_key`, and `request_id` when available. |

Workflow results are JSON on stdout; error envelopes are JSON on stderr. The command reuses the generated CLI's `issues[]` and `next_steps` convention. Invalid arguments or a missing base URL return `INVALID_USAGE` with exit `2`; a missing credential returns `NO_AUTH` with `status: action_required` and exit `1`, before HTTP. Other workflow errors use `CALL_FAILED` with exit `1`. Neither handoffs nor pending work count as success.

```bash
node dist/label-cli.js resume lbl_1
```

Resume only reads the existing label. It never repeats creation. The workflow disables automatic HTTP retries; the example API's contract lets you retry an uncertain create explicitly with the **same shipment and key**. For your API, verify its idempotency guarantees and retention period before adopting that recovery strategy. Persist the key and returned ID in your application's durable state when a process restart must be recoverable.

The mock demonstrates completion and idempotency. The included tests supply the handoff and failure responses. Adapt the workflow's states to your real API: hosted payment frames, authentication challenges, and approvals remain explicit browser or human steps. An OpenAPI description alone cannot determine that an end-to-end business task has completed.

## Preserve it through regeneration

For a linked repository Delivery, add these files and the manifest edits to the Target's rolling Draft. Follow [package check configuration](https://typeship.dev/docs/guides/customize#configure-package-checks) to add a customer check named `parcel-label-workflow` with command `npm run test:workflow`, preserving existing checks. Keep the generated build, package, and public-entrypoint checks enabled.

Try a Definition update: add an optional string property named `carrier` to the `Label` schema and regenerate. Pull the updated Draft, rebuild, and run the workflow tests. The new model field, custom files, `parcel-label` executable, and test script should all remain present. Check the current Draft head's results before merging.

Preserving source does not establish that it still works with a changed API. Removing the label-read operation, for example, leaves a workflow that cannot compile. Resolve incompatible API changes and overlapping edits before accepting the updated package. See [regeneration and conflicts](https://typeship.dev/docs/projects/regeneration#three-way-integration).

Follow [Review an API update](https://typeship.dev/docs/guides/review-an-api-update) to carry this workflow through an additive change, an overlapping edit, and a client method rename in a linked repository, then inspect the accepted Releases.

The initial download above is stateless and has no retained repository baseline. Its extraction process owns later updates. Use a linked repository Delivery for Typeship's preservation and review workflow; see [adding Typeship to an API repository](https://typeship.dev/docs/guides/add-to-an-existing-api-repo).

## Sitemap

[Documentation index](https://typeship.dev/llms.txt)
