> ## Documentation Index
> Fetch the complete documentation index at: https://deepline.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Production Play Integration

> Integrate Deepline plays into a production backend with durable run IDs, resilient polling, callbacks, idempotent writes, and explicit cancellation.

[Human guide: work with your agent →](https://deepline.com/docs/human/sdk-v2/production-integration.md)

> [!NOTE]
>
> Recover the same asynchronous run and apply its accepted result through one
> destination owner.

A Deepline play is an asynchronous job. Starting one returns a public run ID
before the work necessarily finishes. Persist that ID, then use it to observe
the same run until Deepline reports a terminal state.

For direct tool calls, use [tool execution recovery](https://deepline.com/docs/sdk-v2/tool-execution-recovery.md).
Its caller-owned key is available before dispatch, so it also covers a lost
initial HTTP response.

![An application starts a play, persists its run ID, lets Deepline run asynchronously, then recovers the terminal result.](https://mintcdn.com/deepline2/HfX7N0vAcXPGNX65/images/production-play-lifecycle.svg?fit=max\&auto=format\&n=HfX7N0vAcXPGNX65\&q=85\&s=2b1a6adf17eac7d7c9f0362844bb7efe)

The run ID is the durable handoff between your application and Deepline.

> [!WARNING]
>
> A timeout in your HTTP client, request handler, worker lease, or polling loop
> does not stop the Deepline run. Do not start the enrichment again only because
> your local wait ended.

## Choose where the result is written

The best integration depends on who should validate and persist the final
result.

| Pattern | Result owner | Infrastructure | Use when |
| - | - | - | - |
| Persist and poll | Your application | Background worker or scheduled job | You want the smallest reliable starting point |
| Custom-play callback | Your application | Authenticated callback endpoint | You want event-driven completion without continuous polling |
| Direct destination write | Deepline play | Scoped destination credentials | The play can own enrichment and the final write |
| Synchronous provider calls | Your application | Your own orchestration | A request path has a strict latency budget |

Start with persisted polling. Move to a callback when polling is operationally
awkward. Let the play write directly only when that ownership and credential
boundary fit your system.

## Persist and poll

Persist the run ID before waiting. If the process restarts or its local wait
budget ends, a background worker can recover the same run.

```mermaid placement="top-right" theme={"languages":{"custom":["/languages/deepline-prompt.json"]}}
sequenceDiagram
    participant App as Your application
    participant Store as Your database
    participant Deepline

    App->>Deepline: Start play
    Deepline-->>App: Return run ID
    App->>Store: Persist run ID

    loop Until Deepline reports a terminal state
        App->>Deepline: Get status for the same run ID
        Deepline-->>App: queued, running, or waiting
    end

    Deepline-->>App: completed, failed, or cancelled
    App->>Store: Write result once, keyed by run ID
```

### Start the run

`PlayJob.id` is the public run ID. Save it before calling `get()`, `tail()`, or
starting your own polling loop.

```ts theme={"languages":{"custom":["/languages/deepline-prompt.json"]}}
import { Deepline } from 'deepline';

declare const runs: {
  insert(run: { runId: string; status: 'running' }): Promise<void>;
};

const deepline = await Deepline.connect();
const job = await deepline
  .play<{
    domain: string;
    roles: string[];
  }>('prebuilt/company-to-contact')
  .run({
    domain: 'example.com',
    roles: ['VP of Sales'],
  });

await runs.insert({
  runId: job.id,
  status: 'running',
});
```

### Resume the same run

Use `DeeplineClient.getPlayStatus(runId)` when a worker resumes from a stored
run ID.

```ts theme={"languages":{"custom":["/languages/deepline-prompt.json"]}}
import { DeeplineClient } from 'deepline';

declare const storedRun: { runId: string };
declare const results: {
  writeOnce(input: { idempotencyKey: string; value: unknown }): Promise<void>;
};

const client = new DeeplineClient();
const status = await client.getPlayStatus(storedRun.runId, { full: true });

if (status.status === 'completed') {
  await results.writeOnce({
    idempotencyKey: storedRun.runId,
    value: status.result,
  });
}
```

Only `completed`, `failed`, and `cancelled` are terminal. Treat `queued`,
`running`, and `waiting` as active work that can be checked again.

> [!TIP]
>
> Use the run ID as the idempotency key for the destination write. Repeated
> polls, worker restarts, or duplicate completion delivery then converge on one
> stored result.

## Call your application when a custom play finishes

Use a custom play when you want Deepline to finish the enrichment and then make
a durable outbound request to your application. The custom play owns the
callback step. This is distinct from an inbound webhook that starts a play.

```mermaid placement="top-right" theme={"languages":{"custom":["/languages/deepline-prompt.json"]}}
sequenceDiagram
    participant App as Your application
    participant Deepline
    participant Callback as Your callback endpoint
    participant Store as Your database

    App->>Deepline: Start custom play
    Deepline-->>App: Return run ID
    App->>Store: Persist run ID
    Deepline->>Deepline: Complete enrichment
    Deepline->>Callback: POST authenticated result + request ID
    Callback->>Callback: Authenticate and validate payload
    Callback->>Store: Write once by request ID
```

Use `ctx.runPlay(...)` to call a prebuilt play and `ctx.fetch(...)` for the
recorded callback. Keep callback credentials in declared play secrets, require
HTTPS, and reject invalid tokens in your application. Pass a stable application
request ID into the custom play, store its mapping to the Deepline run ID, and
echo it in the callback payload.

Source: `docs-examples/sdk-v2/enrich-and-notify.play.ts`

```ts theme={"languages":{"custom":["/languages/deepline-prompt.json"]}}
import { CtxFetchHttpError, definePlay } from 'deepline';

const APPLICATION_CALLBACK_URL =
  'https://app.example.com/api/deepline/callback';

export default definePlay(
  'enrich-and-notify',
  async (
    ctx,
    input: {
      request_id: string;
      company_domain: string;
      job_title: string;
    },
  ) => {
    const result = await ctx.runPlay(
      'company_to_contact',
      'prebuilt/company-to-contact',
      {
        domain: input.company_domain,
        roles: [input.job_title],
      },
      {
        description: 'Find a matching contact.',
      },
    );

    const callbackToken = await ctx.secrets.get('APP_CALLBACK_TOKEN');
    let lastStatus: number | null = null;
    try {
      await ctx.fetch(
        'notify_application_1',
        APPLICATION_CALLBACK_URL,
        {
          method: 'POST',
          auth: ctx.secrets.bearer(callbackToken),
          headers: {
            'content-type': 'application/json',
            'idempotency-key': input.request_id,
          },
          body: JSON.stringify({ request_id: input.request_id, result }),
        },
        { staleAfterSeconds: 0 },
      );
      return { delivered: true };
    } catch (error) {
      if (error instanceof CtxFetchHttpError) lastStatus = error.status;
      ctx.log('First callback attempt failed.');
    }

    await ctx.sleep(5_000);

    try {
      await ctx.fetch(
        'notify_application_2',
        APPLICATION_CALLBACK_URL,
        {
          method: 'POST',
          auth: ctx.secrets.bearer(callbackToken),
          headers: {
            'content-type': 'application/json',
            'idempotency-key': input.request_id,
          },
          body: JSON.stringify({ request_id: input.request_id, result }),
        },
        { staleAfterSeconds: 0 },
      );
      return { delivered: true };
    } catch (error) {
      if (error instanceof CtxFetchHttpError) lastStatus = error.status;
      ctx.log('Second callback attempt failed.');
    }

    await ctx.sleep(30_000);

    try {
      await ctx.fetch(
        'notify_application_3',
        APPLICATION_CALLBACK_URL,
        {
          method: 'POST',
          auth: ctx.secrets.bearer(callbackToken),
          headers: {
            'content-type': 'application/json',
            'idempotency-key': input.request_id,
          },
          body: JSON.stringify({ request_id: input.request_id, result }),
        },
        { staleAfterSeconds: 0 },
      );
      return { delivered: true };
    } catch (error) {
      if (error instanceof CtxFetchHttpError) lastStatus = error.status;
      ctx.log('Third callback attempt failed.');
    }

    throw new Error(
      lastStatus === null
        ? 'Callback failed without an HTTP response.'
        : `Callback failed with HTTP ${lastStatus}.`,
    );
  },
  {
    description:
      'Find a company contact and deliver the result to the application.',
    secrets: ['APP_CALLBACK_TOKEN'],
  },
);
```

The application should acknowledge the callback only after it has validated
the payload and committed an idempotent write.

## Let the custom play write directly

A custom play can also enrich the record and write the result to the final
destination through a Deepline integration or a recorded `ctx.fetch(...)`
step.

```mermaid placement="top-right" theme={"languages":{"custom":["/languages/deepline-prompt.json"]}}
flowchart LR
    App["Your application"] -->|"Start custom play"| Deepline["Deepline"]
    Deepline -->|"Return run ID"| App
    Deepline --> Enrich["Complete enrichment"]
    Enrich --> Write["Write to your destination"]
    Write --> Done["Run completes"]
```

This removes the polling worker and callback service. It also moves final-write
ownership into the play. Use narrowly scoped credentials and make the
destination operation idempotent.

Choose this pattern when:

* the play owns the complete business transaction.
* your application does not need to validate the result first.
* the destination supports a safe upsert or idempotency key.
* you are comfortable storing scoped destination credentials in Deepline.

## Handle local timeouts

A local wait budget and a remote run state are separate.

| What happened | What to do |
| - | - |
| You received a run ID, then polling timed out | Store the state as active and poll the same run later |
| A worker restarted after storing the run ID | Resume with `getPlayStatus(runId)` |
| The run is `queued`, `running`, or `waiting` | Keep the run active. do not create a replacement |
| The run is `completed` | Validate and write the result once |
| The run is `failed` | Store the failure context and apply your retry policy deliberately |
| The run is `cancelled` | Stop polling and record the cancellation |
| The start request failed before returning a run ID | Reconcile application state before deciding whether to submit again |

Keep the final status response when a run fails. Its run ID, progress, and error
context make support and replay decisions much easier.

## Stop a run intentionally

Ending a local request does not cancel a play. Stop the run explicitly when the
user cancels the underlying job or the result is no longer useful.

```ts theme={"languages":{"custom":["/languages/deepline-prompt.json"]}}
import { DeeplineClient } from 'deepline';

export async function stopRun(runId: string): Promise<void> {
  const client = new DeeplineClient();
  const result = await client.stopPlay(runId, {
    reason: 'Request cancelled by the user',
  });
  if (!result.stopped) {
    throw new Error(result.error ?? `Could not confirm stop for ${runId}`);
  }
}
```

From raw HTTP:

```bash theme={"languages":{"custom":["/languages/deepline-prompt.json"]}}
RUN_ID="<persisted-run-id>"
curl -X POST "$DEEPLINE_HOST_URL/api/v2/runs/$RUN_ID/stop" \
  -H "Authorization: Bearer $DEEPLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Request cancelled by the user"}'
```

Stopping a run is a product decision. Do not use cancellation as the automatic
response to a short polling window.

## Design for a latency-sensitive request path

Prebuilt plays can use multiple providers, retries, and fallback paths to
improve coverage and price-performance. That work can outlive an interactive
request.

For a tighter latency budget, choose one of these:

* fork the prebuilt play and keep only the providers or branches that matter to
  the request path.
* return a pending state to the user and finish enrichment in the background.
* call synchronous provider endpoints and own their rate limits, transient
  errors, retries, caching, and observability in your application.

Do not force a broad enrichment waterfall into a synchronous UI deadline.

## Production checklist

* Persist the run ID before waiting.
* Treat only `completed`, `failed`, and `cancelled` as terminal.
* Resume the same run after timeouts and worker restarts.
* Key final writes by run ID.
* Validate and authenticate callback payloads.
* Scope destination and callback credentials narrowly.
* Stop runs only when cancellation is intentional.
* Keep run IDs and terminal responses in logs and support records.
* Pilot the integration with a small, representative input set before scaling.

## Related guides

> **[Call Plays](https://deepline.com/docs/sdk-v2/calling-plays.md)**
>
> Start plays from TypeScript, the CLI, another play, or raw HTTP.

> **[Background Agents](https://deepline.com/docs/sdk-v2/background-agents.md)**
>
> Run Deepline safely from workers, scheduled jobs, and CI.

> **[HTTP and Python](https://deepline.com/docs/sdk-v2/http-and-python.md)**
>
> Start and poll plays from non-TypeScript systems.

> **[API Reference](https://deepline.com/docs/sdk-v2/api-reference.md)**
>
> Inspect run status, streaming, output, and stop contracts.

## Verify this task

Exercise a caller restart using the stored run ID. Verify the destination observes one intended change under its supported idempotency contract.

Ask your agent to explain a failed check before repeating work or expanding the scope.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.