> ## 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.

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.

<Frame caption="The run ID is the durable handoff between your application and Deepline.">
  <img src="https://mintcdn.com/deepline2/HfX7N0vAcXPGNX65/images/production-play-lifecycle.svg?fit=max&auto=format&n=HfX7N0vAcXPGNX65&q=85&s=2b1a6adf17eac7d7c9f0362844bb7efe" alt="An application starts a play, persists its run ID, lets Deepline run asynchronously, then recovers the terminal result." width="1280" height="360" data-path="images/production-play-lifecycle.svg" />
</Frame>

<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.
</Warning>

## 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={null}
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={null}
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={null}
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.
</Tip>

## 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={null}
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={null}
import { 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.',
      },
    );

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

    await ctx.sleep(5_000);

    try {
      const secondAttempt = await ctx.fetch(
        'notify_application_2',
        APPLICATION_CALLBACK_URL,
        {
          method: 'POST',
          auth: ctx.secrets.bearer(ctx.secrets.get('APP_CALLBACK_TOKEN')),
          headers: {
            'content-type': 'application/json',
            'idempotency-key': input.request_id,
          },
          body: JSON.stringify({ request_id: input.request_id, result }),
        },
      );
      if (secondAttempt.ok) {
        return { delivered: true };
      }
      lastStatus = secondAttempt.status;
    } catch {
      ctx.log('Second callback attempt failed before receiving a response.');
    }

    await ctx.sleep(30_000);

    try {
      const thirdAttempt = await ctx.fetch(
        'notify_application_3',
        APPLICATION_CALLBACK_URL,
        {
          method: 'POST',
          auth: ctx.secrets.bearer(ctx.secrets.get('APP_CALLBACK_TOKEN')),
          headers: {
            'content-type': 'application/json',
            'idempotency-key': input.request_id,
          },
          body: JSON.stringify({ request_id: input.request_id, result }),
        },
      );
      if (thirdAttempt.ok) {
        return { delivered: true };
      }
      lastStatus = thirdAttempt.status;
    } catch {
      ctx.log('Third callback attempt failed before receiving a response.');
    }

    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={null}
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={null}
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={null}
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

<CardGroup cols={2}>
  <Card title="Call Plays" icon="play" href="/sdk-v2/calling-plays">
    Start plays from TypeScript, the CLI, another play, or raw HTTP.
  </Card>

  <Card title="Background Agents" icon="clock" href="/sdk-v2/background-agents">
    Run Deepline safely from workers, scheduled jobs, and CI.
  </Card>

  <Card title="HTTP and Python" icon="code" href="/sdk-v2/http-and-python">
    Start and poll plays from non-TypeScript systems.
  </Card>

  <Card title="API Reference" icon="brackets-curly" href="/sdk-v2/api-reference">
    Inspect run status, streaming, output, and stop contracts.
  </Card>
</CardGroup>
