/** * Play definition, execution, and lifecycle management. * * This module provides the high-level API for defining and running plays — * composable TypeScript workflows that orchestrate tool calls into repeatable * pipelines. Plays run on Temporal for durable execution with automatic retries, * timeouts, and full observability. * * ## Core concepts * * - **{@link definePlay}** — Define a play as a typed async function. The returned * value is both callable (for server-side execution) and has methods for * remote lifecycle management (run, get, publish, tail). * * - **{@link Deepline.connect}** — Create a client context for programmatic SDK * usage. Returns a {@link DeeplineContext} with tool and play handles. * * - **{@link PlayJob}** — Handle to a running play execution. Poll for status, * stream logs, wait for completion, or cancel. * * ## Usage patterns * * ### Define and run a play (file-backed) * * ```typescript * // my-play.play.ts * import { definePlay } from 'deepline'; * * export default definePlay('my-play', async (ctx, input: { domain: string }) => { * ctx.log(`Looking up ${input.domain}`); * const company = await ctx.tools.execute({ * id: 'company_search', * tool: 'test_company_search', * input: { domain: input.domain }, * description: 'Look up company details by domain.', * }); * return company; * }); * ``` * * Then run via the CLI: * `deepline plays run --file my-play.play.ts --watch`. * Installed npm CLIs can also run saved or prebuilt plays by name. * * ### Programmatic usage * * ```typescript * import { Deepline } from 'deepline'; * * const deepline = await Deepline.connect(); * const job = await deepline.play('my-play').run({ domain: 'stripe.com' }); * const result = await job.get(); // Polls until complete * ``` * * ### With bindings (cron, webhook) * * ```typescript * import { definePlay } from 'deepline'; * * export default definePlay('daily-sync', async (ctx) => { * const data = await ctx.tools.execute({ * id: 'crm_export', * tool: 'crm_export', * input: {}, * description: 'Export CRM records for the daily sync.', * }); * return data; * }, { * cron: { schedule: '0 9 * * *', timezone: 'America/New_York' }, * }); * ``` * * @module */ import { DeeplineClient } from './client.js'; import { DeeplineError } from './errors.js'; import { attachToolResultListDataset, createToolExecuteResult, } from '../../shared_libs/play-runtime/tool-result.js'; export { readValue, readList, } from '../../shared_libs/play-runtime/tool-result.js'; import { createDeferredPlayDataset } from '../../shared_libs/plays/dataset.js'; import { QUERY_RESULT_DATASET_PAGE_SIZE, isCustomerDbDatasetTool, isQueryResultDatasetTool, } from '../../shared_libs/play-runtime/query-result-dataset.js'; import type { PlayDataset, PlayDatasetInput, PlayDatasetRow, } from '../../shared_libs/plays/dataset.js'; import type { ToolExecuteResult, ToolResultMetadataInput, } from '../../shared_libs/play-runtime/tool-result-types.js'; import type { EmailStatusExtractorConfig } from '../../shared_libs/play-runtime/email-status.js'; import type { DeeplineClientOptions, PlayDetail, ClearPlayHistoryResult, PlayListItem, PlayRevisionSummary, PlayRunListItem, PlayStatus, PublishPlayVersionResult, StopPlayRunResult, ToolDefinition, ToolMetadata, } from './types.js'; import type { ToolExecution } from './client.js'; import type { DurableCallStaleAfterSeconds, PlayAuthoringBindings, PlayAuthoringCallExecution, PlayAuthoringCallOptions, PlayAuthoringColumnResolver, PlayAuthoringColumnMap, PlayAuthoringConditionalStepResolver, PlayAuthoringCsvInput, PlayAuthoringCsvOptions, PlayAuthoringCsvRenameMap, PlayAuthoringDatasetBuilder, PlayAuthoringDatasetColumnDefinition, PlayAuthoringDatasetColumnRunInput, PlayAuthoringDatasetDefinitionOptions, PlayAuthoringDatasetRowKey, PlayAuthoringDatasetRunOptions, PlayAuthoringDefineConfig, PlayAuthoringDefinedPlay, PlayAuthoringInputContract, PlayAuthoringFileInput, PlayAuthoringFetchOptions, PlayAuthoringFetchResponse, PlayAuthoringReferenceLike, PlayAuthoringRunnableStepProgram, PlayAuthoringRuntimeContext, PlayAuthoringRuntimeStepOptions, PlayAuthoringStepOptions, PlayAuthoringStepProgramOptions, PlayAuthoringStepProgram, PlayAuthoringStepProgramOutput, PlayAuthoringStepProgramResolver, PlayAuthoringStepProgramStep, PlayAuthoringStepResolver, PlayLooseObject, PlayReturnObject as PlayAuthoringReturnObject, PlaySecretAuth, PlaySecretAuthInput, PlaySecretAwareRequestInit, PlaySecretHandle, PlaySecretPromise, PlaySecretValue, PlaySqlQuery, PlayReceiptWaitMs, PlayRuntimeTimeoutMs, PlaySqlListenerDeclaration, PlaySqlListenerEvent, PlaySqlListenerFilterOperator, PlaySqlListenerFilterScalar, PlaySqlListenerOperation, PlaySqlListenerWhere, PlayToolExecutionRequest, } from '../../shared_libs/plays/authoring-contract.js'; import { createPlayInputContract } from '../../shared_libs/plays/input-contract-definition.js'; export type PlayCallExecution = PlayAuthoringCallExecution; export type PlayCallOptions = PlayAuthoringCallOptions; export type RuntimeStepOptions = PlayAuthoringRuntimeStepOptions; export type FetchOptions = PlayAuthoringFetchOptions; export type PlayFetchResponse = PlayAuthoringFetchResponse; /** * Optional Play configuration, including triggers and runtime limits. * * A play can be triggered three ways, declared as the third argument to * {@link definePlay}: * - `webhook` — an inbound HTTP call (with optional legacy HMAC or Standard * Webhooks signature verification); * - `cron` — a schedule; or * - `sqlListeners` — a **monitor**: the play runs whenever a monitor writes a new * row to its output stream. This is how you build a play "on top of" a monitor * (e.g. run enrichment every time a watched company posts a new job). Each * listener binds to a monitor tool id + one of its output stream keys (see * `deepline monitors available ` for a tool's streams and row columns). * The changed row is delivered to the handler as the listener event's `after`. * * The default Play runtime is 30 minutes. For bounded long-running batches, add * `runtime: { timeout: '90m', size: 'standard' }`; duration values are whole minutes or hours, up to `4h`. * It differs from `ctx.tools.execute({ timeoutMs })`, which limits one provider call. * * @example Webhook with HMAC verification * ```typescript * definePlay('webhook-handler', handler, { * webhook: { * hmac: { * algorithm: 'sha256', * header: 'X-Hub-Signature-256', * secretEnv: 'WEBHOOK_SECRET', * }, * }, * }); * ``` * * @example Svix / Standard Webhooks verification with Deepline Secrets * ```typescript * definePlay('visitor-webhook', handler, { * webhook: { * auth: { * type: 'standard-webhooks', * headerFamily: 'svix', * signingSecrets: ['VECTOR_WEBHOOK_SECRET'], * }, * }, * }); * ``` * * @example Cron schedule * ```typescript * definePlay('nightly-sync', handler, { * cron: { schedule: '0 2 * * *', timezone: 'UTC' }, * }); * ``` * * @example Monitor-triggered (run a play on a monitor's output) * ```typescript * definePlay('on-new-job-opening', handler, { * sqlListeners: [ * { * id: 'jobs', * tool: 'deepline_native.company_radar', * stream: 'company_job_openings', * operations: ['INSERT'], * }, * ], * }); * ``` * * @sdkReference runtime 030 */ export type PlayBindings = PlayAuthoringBindings; export type SqlListenerOperation = PlaySqlListenerOperation; export type SqlListenerFilterScalar = PlaySqlListenerFilterScalar; export type SqlListenerFilterOperator = PlaySqlListenerFilterOperator; export type SqlListenerWhere = PlaySqlListenerWhere; export type SqlListenerDeclaration = PlaySqlListenerDeclaration; export type SqlListenerEvent> = PlaySqlListenerEvent; /** @deprecated Pass a SQL string directly to ctx.customerDb.query. */ export type SqlQuery = PlaySqlQuery; export type SecretHandle = PlaySecretHandle; export type SecretPromise = PlaySecretPromise; export type SecretValue = PlaySecretValue; export type SecretAuth = PlaySecretAuth; export type SecretAuthInput = PlaySecretAuthInput; export type SecretAwareRequestInit = PlaySecretAwareRequestInit; export type LoosePlayObject = PlayLooseObject; export type { ToolExecuteResult, ToolExecuteResultAccessors, ToolExecuteResultBase, ToolResultEnvelope, ToolResultListAccessor, ToolResultTargetAccessor as ToolExtractedValue, } from '../../shared_libs/play-runtime/tool-result-types.js'; export { DEEPLINE_EXTRACTOR_TARGETS, DEEPLINE_EXTRACTOR_TARGET_DEFINITIONS, JOB_CHANGE_STATUS_VALUES, PHONE_STATUS_VALUES, isDeeplineExtractorTarget, } from '../../shared_libs/play-runtime/extractor-targets.js'; export type { DeeplineEmailStatusGetterValue, DeeplineExtractorTarget, DeeplineGetterValue, DeeplineGetterValueMap, JobChangeStatus, PhoneStatus, } from '../../shared_libs/play-runtime/extractor-targets.js'; export type { PreviousCell } from '../../shared_libs/play-runtime/cell-staleness.js'; /** * Keyword-style request object for `ctx.tools.execute(...)`. * * The `tool` value comes from live tool discovery. The `id` is the stable * logical call name used for logs, metadata, and receipt attachment. Provider * call reuse is keyed by play, tool, semantic input, auth scope, provider action * version, and cache policy. * * @sdkReference runtime 160 */ export type ToolExecutionRequest = PlayToolExecutionRequest; export type StepResolver = PlayAuthoringStepResolver< Row, Value, DeeplinePlayRuntimeContext >; /** * Input object passed to an object-column `run` resolver. * * @sdkReference runtime 090 */ export type DatasetColumnRunInput = PlayAuthoringDatasetColumnRunInput; /** * Object-column form for `.withColumn(...)`. * * Use this when a column needs `runIf` or typed `previousCell`. * * @sdkReference runtime 100 */ export type DatasetColumnDefinition = PlayAuthoringDatasetColumnDefinition; export type ConditionalStepResolver< Row, Value, Else = null, > = PlayAuthoringConditionalStepResolver< Row, Value, DeeplinePlayRuntimeContext, Else >; /** * Options for row-level `.withColumn(...)` and `steps().step(...)` entries. * * @sdkReference runtime 110 */ export type StepOptions = PlayAuthoringStepOptions< Row, Value >; /** Explicitly mark a step program as a provider fallback waterfall. */ export type StepProgramOptions = PlayAuthoringStepProgramOptions; export type StepProgram< Input, Output, Return = Output, > = PlayAuthoringStepProgram; export type StepProgramResolver = PlayAuthoringStepProgramResolver; export type PlayStepProgramStep = PlayAuthoringStepProgramStep; export type ColumnResolver = PlayAuthoringColumnResolver< Row, Value, DeeplinePlayRuntimeContext >; export type StepProgramOutput = PlayAuthoringStepProgramOutput; export type RunnableStepProgram = Pick< PlayAuthoringRunnableStepProgram, 'kind' | 'steps' | 'returnResolver' > & { readonly __inputType?: (input: TInput) => void }; /** * Builder returned by `ctx.dataset(...)` for row-level durable columns. * * @sdkReference runtime 070 .dataset(...).withColumn(name, resolver).run(options) */ export type DatasetBuilder< InputRow extends object, OutputRow extends object, > = PlayAuthoringDatasetBuilder< InputRow, OutputRow, DeeplinePlayRuntimeContext >; export type DatasetRowKey = PlayAuthoringDatasetRowKey; export type DatasetDefinitionOptions = PlayAuthoringDatasetDefinitionOptions; export type DatasetRunOptions = PlayAuthoringDatasetRunOptions; export type CsvRenameMap = PlayAuthoringCsvRenameMap; /** * Runtime file-like input. At runtime this is the staged file path/reference * string. The type parameter carries static metadata for describe/CLI tooling. */ export type FileInput = PlayAuthoringFileInput; /** * CSV file input whose rows are described by `TRow`. * * The CLI should expose this as the field name from the play input object, * stage local paths passed to that flag, and use `TRow` for row-contract * discovery. */ export type CsvInput> = PlayAuthoringCsvInput; export type ColumnMap = PlayAuthoringColumnMap; /** * Options for loading a staged CSV with `ctx.csv(...)`. * * @sdkReference runtime 050 */ export type CsvOptions = PlayAuthoringCsvOptions; /** * Runtime context available inside a play function. * * Provides methods for calling tools, processing data, and emitting logs. * This context is injected by the Temporal worker — you never construct it directly. * * @example * ```typescript * definePlay('example', async (ctx, input: { domain: string; csv: string }) => { * // Call a tool * const company = await ctx.tools.execute({ * id: 'company_search', * tool: 'test_company_search', * input: { domain: input.domain }, * description: 'Look up company details by domain.', * }); * * // Fan-out: process items with named columns * const enriched = await ctx * .dataset('companies', [{ domain: 'a.com' }, { domain: 'b.com' }]) * .withColumn('company', (row, rowCtx) => * rowCtx.tools.execute({ * id: 'company_search', * tool: 'test_company_search', * input: { domain: row.domain }, * description: 'Look up company details by domain.', * })) * .run({ description: 'Look up company details.' }); * * // Load CSV data from a submitted play input field. * const leads = await ctx.csv(input.csv); * * // Emit a log line (visible in `play tail`) * ctx.log(`Loaded ${await leads.count()} leads`); * * // Pause execution * await ctx.sleep(1000); * * // Access submitted input through the handler's second argument. * ctx.log(`Running for ${input.domain}`); * * return { company, enriched }; * }); * ``` */ export type DeeplinePlayRuntimeContext = PlayAuthoringRuntimeContext; /** * Handle to a running play execution. * * Provides methods to check status, stream logs, wait for completion, * or cancel the execution. * * This handle is the SDK-context equivalent of `deepline plays run --watch` and * `POST /api/v2/plays/run`: every surface returns a run id first, then exposes * the completed user output through `PlayJob.get()` or the status endpoint's * `result` field. Runtime logs are available from `status().progress.logs` and * are intentionally separate from the returned output object. * * @typeParam TOutput - The play's return type * * @example * ```typescript * const job: PlayJob = await ctx.play('my-play').run({ domain: 'stripe.com' }); * * // Poll status * const status = await job.status(); * console.log(status.status); // 'running' * * // Stream logs until completion * const finalStatus = await job.tail({ * onLog: (line) => console.log(`[play] ${line}`), * }); * * // Or just wait for the result * const output = await job.get(); * * // Cancel if needed * await job.cancel(); * ``` * * @sdkReference plays 030 */ export interface PlayJob { /** Temporal workflow ID for this execution. */ id: string; /** Get the current execution status (single poll). */ status(): Promise; /** * Stream logs and wait for completion. * * Polls until the play reaches a terminal state, invoking `onLog` for * each new log line. Returns the final status. * * @param options.intervalMs - Poll interval in ms. Default: `500`. * @param options.onLog - Callback for each log line. Default: `console.log`. */ tail(options?: { intervalMs?: number; onLog?: (line: string) => void; }): Promise; /** * Wait for the play to complete and return its output. * * Polls until terminal state. Throws {@link DeeplineError} if the play * fails, is cancelled, or times out. * * @param options.intervalMs - Poll interval in ms. Default: `500`. * @returns The play's return value * @throws {@link DeeplineError} if the play did not complete successfully */ get(options?: { intervalMs?: number }): Promise; /** Cancel this play execution. */ cancel(): Promise; /** Deep-stop this play execution, including open HITL waits. */ stop(options?: { reason?: string }): Promise; } /** * Handle to a named play for remote lifecycle operations. * * Returned by {@link DeeplineContext.play} and attached to {@link DefinedPlay}. * Provides methods to run, inspect, list runs, and publish a play by name. * * @typeParam TInput - The play's input type * @typeParam TOutput - The play's return type * * @example * ```typescript * const ctx = await Deepline.connect(); * const play = ctx.play<{ domain: string }, Company>('company-lookup'); * * // Get play definition * const detail = await play.get(); * console.log(`Live: v${detail.play.currentPublishedVersion}`); * * // Run and wait * const result = await play.runSync({ domain: 'stripe.com' }); * * // Run async * const job = await play.run({ domain: 'stripe.com' }); * const output = await job.get(); * * // List recent runs * const runs = await play.runs(); * * // List saved versions * const versions = await play.versions(); * * // Publish the current draft * await play.publish(); * ``` * * @sdkReference plays 020 */ export interface DeeplineNamedPlay< TInput = Record, TOutput = unknown, > { /** The play's name. */ readonly name: string; /** Fetch the full play definition with revision history and run stats. */ get(): Promise; /** List recent runs for this play. */ runs(): Promise; /** List saved versions for this play (newest first). */ versions(): Promise; /** Publish a play revision. Defaults to the current working revision. */ publish(options?: { revisionId?: string }): Promise; /** * Clear run history and durable sheet/result data for this play while keeping * the play definition and revisions. */ clearHistory(options?: { tableNamespaces?: string[]; }): Promise; /** * Start a new run of this play. Returns a {@link PlayJob} for monitoring. * * @param input - Runtime input passed to the play function */ run( input: TInput, options?: { revisionId?: string; profile?: string }, ): Promise>; /** * Run this play and wait for completion. * * Equivalent to `play.run(input).then(job => job.get())`. * * @param input - Runtime input * @returns The play's return value */ runSync( input: TInput, options?: { revisionId?: string; profile?: string }, ): Promise; } /** * Tool/provider operations available from a connected {@link DeeplineContext}. * * This namespace is for regular SDK callers outside a play runtime. Inside a * `definePlay(...)` body, use `ctx.tools.execute({ id, tool, input, ... })` * so provider calls become durable runtime checkpoints. * * @sdkReference tools 010 DeeplineContext.tools */ export type DeeplineToolsNamespace = { /** List all available provider-backed tools. */ list(): Promise; /** Get detailed metadata for one provider-backed tool. */ get(toolId: string): Promise; /** * Execute a provider-backed tool from a regular SDK process. * * For durable play code, prefer `ctx.tools.execute(...)` because the play * runtime records the call under a stable id. */ execute( toolId: string, input: Record, ): Promise; }; /** * Named-play discovery and handle operations from a connected {@link DeeplineContext}. * * @sdkReference plays 010 DeeplineContext.plays */ export type DeeplinePlaysNamespace = { /** List saved and callable plays visible to the current workspace. */ list(): Promise; /** Return a typed handle for a named, saved, shared, or prebuilt play. */ get, TOutput = unknown>( name: string, ): DeeplineNamedPlay; }; export type PrebuiltPlayRef = { readonly playName: string; readonly name: string; }; export type PlayReferenceLike = PlayAuthoringReferenceLike; export type { PlayDataset, PlayDatasetInput, PlayDatasetRow, } from '../../shared_libs/plays/dataset.js'; export type PlayReturnObject = PlayAuthoringReturnObject; export type PlayInputContract = PlayAuthoringInputContract; /** * Object-form play definition accepted by `definePlay(config)`. * * Use this form when the input contract should be explicit at definition time * through `defineInput(schema)`, or when configuration reads clearer as one * object. The shorthand `definePlay(name, fn, bindings?)` is equivalent for * simple file-backed plays. * * @sdkReference runtime 020 */ export type DefinePlayConfig< TInput, TOutput extends PlayReturnObject, > = PlayAuthoringDefineConfig; class DeeplineConditionalStepResolver< Row, Value, ElseValue, > implements ConditionalStepResolver { readonly kind = 'conditional' as const; constructor( readonly when: (row: Row, index: number) => boolean | Promise, readonly run: StepResolver, readonly elseValue: ElseValue, ) {} else( value: ValueElse, ): ConditionalStepResolver { return new DeeplineConditionalStepResolver(this.when, this.run, value); } } class DeeplineStepProgram implements StepProgram< Input, Output, ReturnValue > { readonly kind = 'steps' as const; declare readonly __inputType?: (input: Input) => void; constructor( readonly steps: readonly PlayStepProgramStep[], readonly returnResolver?: StepResolver, readonly continueOnProviderUnavailable = false, ) {} step( name: Name, resolver: | StepResolver | ConditionalStepResolver | StepProgramResolver, ): StepProgram, ReturnValue>; step( name: Name, resolver: StepResolver | StepProgramResolver, options: StepOptions, ): StepProgram, ReturnValue>; step( name: Name, resolver: | StepResolver | ConditionalStepResolver | StepProgramResolver, options?: StepOptions, ): StepProgram, ReturnValue> { if (!name.trim()) { throw new Error( 'steps().step(name, ...) requires a non-empty step name.', ); } const stepResolver = options?.runIf && !isConditionalStepResolver(resolver) ? new DeeplineConditionalStepResolver( options.runIf, resolver as StepResolver, null, ) : resolver; return new DeeplineStepProgram( [ ...this.steps, { name, ...(options?.recompute === true ? { recompute: true } : {}), ...(options?.recomputeOnError === true ? { recomputeOnError: true } : {}), ...(typeof options?.staleAfterSeconds === 'number' ? { staleAfterSeconds: options.staleAfterSeconds } : {}), resolver: stepResolver as PlayStepProgramStep['resolver'], }, ], this.returnResolver as StepResolver< Output & Record, ReturnValue >, this.continueOnProviderUnavailable, ); } return( resolver: StepResolver, ): StepProgram { return new DeeplineStepProgram( this.steps, resolver, this.continueOnProviderUnavailable, ); } } function isConditionalStepResolver( value: unknown, ): value is ConditionalStepResolver { return ( value !== null && typeof value === 'object' && (value as { kind?: unknown }).kind === 'conditional' ); } export function steps( options: StepProgramOptions = {}, ): StepProgram { return new DeeplineStepProgram( [], undefined, options.continueOnProviderUnavailable === true, ); } export function runIf( predicate: (row: Row, index: number) => boolean | Promise, resolver: StepResolver, ): ConditionalStepResolver { return new DeeplineConditionalStepResolver(predicate, resolver, null); } /** * A defined play: both a callable function and a named play handle. * * Created by {@link definePlay}. Can be: * 1. Called directly as a function (for server-side Temporal execution) * 2. Used as a {@link DeeplineNamedPlay} for remote lifecycle operations * * @typeParam TInput - The play's input type * @typeParam TOutput - The play's return type * * @example * ```typescript * import { definePlay } from 'deepline'; * * const myPlay = definePlay('my-play', async (ctx, input: { domain: string }) => { * return await ctx.tools.execute({ * id: 'company_search', * tool: 'test_company_search', * input: { domain: input.domain }, * description: 'Look up company details by domain.', * }); * }); * * // Type is: DefinedPlay<{ domain: string }, unknown> * * // Use as named play handle: * const detail = await myPlay.get(); * const result = await myPlay.runSync({ domain: 'stripe.com' }); * * // Access metadata: * console.log(myPlay.playName); // "my-play" * console.log(myPlay.bindings); // undefined (no cron/webhook) * ``` */ export type DefinedPlay< TInput, TOutput extends PlayReturnObject, > = PlayAuthoringDefinedPlay< TInput, TOutput, DeeplinePlayRuntimeContext, DeeplineNamedPlay >; type PlayMetadata = { name: string; description?: string; bindings?: PlayBindings; inputSchema?: Record; billing?: PlayBindings['billing']; runtime?: PlayBindings['runtime']; compatibility?: PlayBindings['compatibility']; }; const PLAY_METADATA_SYMBOL = Symbol.for('deepline.play.metadata'); class DeeplinePlayJobImpl implements PlayJob { readonly id: string; constructor( private readonly client: DeeplineClient, runId: string, ) { this.id = runId; } async status(): Promise { return this.client.getPlayStatus(this.id); } async tail(options?: { intervalMs?: number; onLog?: (line: string) => void; }): Promise { const intervalMs = options?.intervalMs ?? 500; const onLog = options?.onLog ?? ((line: string) => console.log(line)); const terminalStates = new Set(['completed', 'failed', 'cancelled']); let lastLogIndex = 0; while (true) { const status = await this.status(); const logs = status.progress?.logs ?? []; for (let index = lastLogIndex; index < logs.length; index += 1) { onLog(logs[index]!); } lastLogIndex = logs.length; if (terminalStates.has(status.status)) { return status; } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } } async get(options?: { intervalMs?: number }): Promise { const intervalMs = options?.intervalMs ?? 500; const terminalStates = new Set(['completed', 'failed', 'cancelled']); while (true) { const status = await this.status(); if (terminalStates.has(status.status)) { if (status.status !== 'completed') { throw new DeeplineError( status.progress?.error || `Play run ${this.id} ended with ${status.status}.`, ); } if (status.package) { return status.package as TOutput; } const payload = status.result as { output?: unknown } | undefined; return ((payload && 'output' in payload ? payload.output : status.result) ?? null) as TOutput; } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } } async cancel(): Promise { await this.client.cancelPlay(this.id); } async stop(options?: { reason?: string }): Promise { return this.client.stopPlay(this.id, options); } } function createNamedPlayHandle< TInput = Record, TOutput = unknown, >( clientFactory: () => DeeplineClient, name: string, ): DeeplineNamedPlay { return { name, get: () => clientFactory().getPlay(name), runs: () => clientFactory().listPlayRuns(name), versions: () => clientFactory().listPlayVersions(name), publish: (options) => clientFactory().publishPlayVersion(name, options), clearHistory: (options) => clientFactory().clearPlayHistory(name, options), async run( input: TInput, options?: { revisionId?: string; profile?: string }, ): Promise> { const client = clientFactory(); const started = await client.startPlayRun({ name, ...(options?.revisionId ? { revisionId: options.revisionId } : {}), ...(options?.profile ? { profile: options.profile } : {}), input: input as Record, }); return new DeeplinePlayJobImpl(client, started.workflowId); }, async runSync( input: TInput, options?: { revisionId?: string; profile?: string }, ): Promise { const job = await this.run(input, options); return job.get(); }, }; } /** * High-level SDK context with tool shortcuts and play handles. * * Created by {@link Deepline.connect}. Wraps a {@link DeeplineClient} with * a friendlier API for common operations. * * @example * ```typescript * const deepline = await Deepline.connect(); * * // Tools * const tools = await deepline.tools.list(); * const result = await deepline.tools.execute('test_company_search', { domain: 'stripe.com' }); * * // Plays * const job = await deepline.play('email-waterfall').run({ domain: 'stripe.com' }); * const output = await job.get(); * ``` * * @sdkReference entrypoints 020 */ export class DeeplineContext { private readonly client: DeeplineClient; /** * Create a high-level SDK context. * * Most callers should use {@link Deepline.connect}; direct construction is * equivalent when you already have explicit client options. * * @param options - Optional SDK client configuration. */ constructor(options?: DeeplineClientOptions) { this.client = new DeeplineClient(options); } /** * Tool operations namespace. * * @example * ```typescript * const tools = await deepline.tools.list(); * const meta = await deepline.tools.get('dropleads_search_people'); * const companyLookup = await deepline.tools.execute('test_company_search', { domain: 'stripe.com' }); * const company = companyLookup.toolResponse.raw; * ``` */ get tools(): DeeplineToolsNamespace { return { /** List all available tools. */ list: (): Promise => this.client.listTools(), /** Get detailed metadata for a tool. */ get: (toolId: string): Promise => this.client.getTool(toolId), /** Execute a tool and return the standard execution envelope. */ execute: async ( toolId: string, input: Record, ): Promise => { const response = await this.client.executeTool(toolId, input, { includeToolMetadata: true, responseIntent: 'dataset', }); return toolExecutionEnvelopeToResult(toolId, response, { client: this.client, input, }); }, }; } /** * Play discovery and named-play handles. * * Use `plays.list()` for discovery and `plays.get(name)` when you prefer a * namespace spelling over `ctx.play(name)`. */ get plays(): DeeplinePlaysNamespace { return { list: () => this.client.listPlays(), get: , TOutput = unknown>( name: string, ) => this.play(name), }; } /** * Convenience references for Deepline-managed prebuilt plays. * * Known prebuilts are exposed by camel-cased aliases. Any other property is * converted into `prebuilt/` so callers can pass the reference to * `ctx.runPlay(...)`. */ get prebuilt(): Record { const explicit = { companyToContact: { playName: 'prebuilt/company-to-contact', name: 'prebuilt/company-to-contact', }, personToPhone: { playName: 'prebuilt/person-to-phone', name: 'prebuilt/person-to-phone', }, personToEmail: { playName: 'prebuilt/person-to-email', name: 'prebuilt/person-to-email', }, personLinkedinToEmail: { playName: 'prebuilt/person-linkedin-to-email', name: 'prebuilt/person-linkedin-to-email', }, } satisfies Record; return new Proxy( {}, { get: (_target, prop) => { if (typeof prop !== 'string') return undefined; if (prop in explicit) { return explicit[prop as keyof typeof explicit]; } const playName = prop.startsWith('prebuilt/') ? prop : `prebuilt/${prop}`; return { playName, name: playName, } satisfies PrebuiltPlayRef; }, }, ) as Record; } /** * Get a named play handle for remote lifecycle operations. * * @typeParam TInput - Expected input type * @typeParam TOutput - Expected output type * @param name - Play name (as registered on the server) * @returns Named play handle with run, versions, get, publish, etc. * * @example * ```typescript * const play = ctx.play<{ domain: string }>('email-waterfall'); * const job = await play.run({ domain: 'stripe.com' }); * const result = await job.get(); * ``` */ play, TOutput = unknown>( name: string, ): DeeplineNamedPlay { return createNamedPlayHandle(() => this.client, name); } /** * Run a named or prebuilt play and wait for its output. * * This is the high-level SDK equivalent of `ctx.play(name).runSync(input)`. * Inside a play runtime, prefer the in-play `ctx.runPlay(key, playRef, input, * options)` form so the child run is checkpointed under a stable key. * * @param playOrRef - Play name or prebuilt/reference object. * @param input - JSON input passed to the play. * @returns Completed play output. */ async runPlay, TOutput = unknown>( playOrRef: string | PlayReferenceLike, input: TInput, ): Promise { const name = typeof playOrRef === 'string' ? playOrRef : (playOrRef.playName ?? playOrRef.name ?? ''); return await this.play(name).runSync(input); } } /** * Static entry point for the Deepline SDK. * * @example * ```typescript * import { Deepline } from 'deepline'; * * const deepline = await Deepline.connect(); * const tools = await deepline.tools.list(); * const result = await deepline.tools.execute('test_company_search', { domain: 'stripe.com' }); * ``` * * @sdkReference entrypoints 010 */ export class Deepline { /** * Create a connected SDK context. * * Resolves configuration from options, environment variables, and CLI config * files. See {@link resolveConfig} for the resolution order. * * @param options - Optional overrides for API key, base URL, etc. * @returns Ready-to-use SDK context * @throws {@link ConfigError} if no API key can be resolved * * @example * ```typescript * // Auto-config (uses env vars / CLI auth): * const ctx = await Deepline.connect(); * * // Explicit config: * const ctx2 = await Deepline.connect({ * apiKey: 'dl_test_...', * baseUrl: 'http://localhost:3000', * }); * ``` */ static async connect( options?: DeeplineClientOptions, ): Promise { return new DeeplineContext(options); } } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } function stringArrayRecord(value: unknown): Record { if (!isRecord(value)) return {}; return Object.fromEntries( Object.entries(value).map(([key, paths]) => [ key, Array.isArray(paths) ? paths.map(String) : [], ]), ); } function stringArray(value: unknown): string[] { return Array.isArray(value) ? value.map(String) : []; } function emailStatusExtractorConfig( value: unknown, ): EmailStatusExtractorConfig | undefined { if (!isRecord(value)) return undefined; const readPaths = (key: string): string[] | undefined => { const paths = stringArray(value[key]) .map((path) => path.trim()) .filter(Boolean); return paths.length > 0 ? paths : undefined; }; const provider = typeof value.provider === 'string' && value.provider.trim() ? value.provider.trim() : null; if (!provider) return undefined; const config: EmailStatusExtractorConfig = { provider }; for (const key of [ 'rawStatus', 'rawScore', 'valid', 'deliverability', 'catchAll', 'mxProvider', 'mxRecord', 'fraudScore', 'disposable', 'roleBased', 'freeEmail', 'abuse', 'spamtrap', 'suspect', ] as const) { const paths = readPaths(key); if (paths) config[key] = paths; } if (isRecord(value.statusMap)) { config.statusMap = value.statusMap as EmailStatusExtractorConfig['statusMap']; } if (Array.isArray(value.rules)) { config.rules = value.rules as EmailStatusExtractorConfig['rules']; } return config; } function extractorDescriptorRecord( value: unknown, ): ToolResultMetadataInput['extractors'] { if (!isRecord(value)) return {}; return Object.fromEntries( Object.entries(value).flatMap(([key, descriptor]) => { if (!isRecord(descriptor)) return []; const paths = stringArray(descriptor.paths) .map((path) => path.trim()) .filter(Boolean); if (paths.length === 0) return []; const transforms = stringArray(descriptor.transforms) .map((transform) => transform.trim()) .filter(Boolean); const enumValues = stringArray(descriptor.enum) .map((entry) => entry.trim()) .filter(Boolean); const emailStatus = emailStatusExtractorConfig(descriptor.emailStatus); return [ [ key, { paths, ...(transforms.length > 0 ? { transforms } : {}), ...(enumValues.length > 0 ? { enum: enumValues } : {}), ...(emailStatus ? { emailStatus } : {}), }, ], ]; }), ); } function rowsFromUnknown(value: unknown): Record[] { if (!Array.isArray(value)) return []; return value.map((row) => (isRecord(row) ? row : { value: row })); } function finiteNonNegativeInteger(value: unknown): number | null { if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) { return null; } return Math.floor(value); } function finitePositiveInteger(value: unknown): number | null { const integer = finiteNonNegativeInteger(value); return integer !== null && integer > 0 ? integer : null; } function stableHash(value: string): string { let hash = 0; for (let index = 0; index < value.length; index += 1) { hash = Math.imul(31, hash) + value.charCodeAt(index); hash |= 0; } return Math.abs(hash).toString(36); } function attachSdkQueryResultDatasetResult( toolId: string, result: ToolExecuteResult, options?: { client: DeeplineClient; input: Record; }, ): ToolExecuteResult { if (!options || !isQueryResultDatasetTool(toolId)) return result; const raw = isRecord(result.toolResponse.raw) ? result.toolResponse.raw : null; const dataset = isRecord(raw?.dataset) ? raw.dataset : null; const totalRows = finiteNonNegativeInteger(dataset?.total_rows); const sql = typeof options.input.sql === 'string' ? options.input.sql : typeof options.input.query === 'string' ? options.input.query : typeof raw?.sql === 'string' ? raw.sql : null; if (!dataset || totalRows === null || !sql) { return result; } const datasetLimit = finitePositiveInteger(dataset.returned_limit) ?? totalRows; const effectiveCount = Math.min(totalRows, datasetLimit); const rawRows = rowsFromUnknown(raw?.rows); const previewRows = rawRows.slice(0, 25); const fetchPage = async ( offset: number, limit: number, ): Promise[]> => { if (limit <= 0 || offset >= totalRows) return []; const response = await options.client.executeTool(toolId, options.input, { includeToolMetadata: true, responseIntent: 'dataset', metadata: { query_result_dataset: { limit: datasetLimit, offset, page_size: Math.min(limit, QUERY_RESULT_DATASET_PAGE_SIZE), total_rows: totalRows, }, ...(isCustomerDbDatasetTool(toolId) ? { customer_db_dataset: { limit: datasetLimit, offset, page_size: Math.min(limit, QUERY_RESULT_DATASET_PAGE_SIZE), total_rows: totalRows, }, } : {}), }, }); const pageRaw = isRecord(response.toolResponse?.raw) ? response.toolResponse.raw : null; return rowsFromUnknown(pageRaw?.rows); }; const collectRows = async ( limit: number | undefined, ): Promise[]> => { const target = Math.min(limit ?? totalRows, totalRows, datasetLimit); const collected: Record[] = []; for ( let offset = 0; offset < target; offset += QUERY_RESULT_DATASET_PAGE_SIZE ) { collected.push( ...(await fetchPage( offset, Math.min(QUERY_RESULT_DATASET_PAGE_SIZE, target - offset), )), ); } return collected.slice(0, target); }; const executionNonce = typeof result.job_id === 'string' && result.job_id.trim() ? result.job_id.trim() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; const safeNonce = executionNonce .replace(/[^A-Za-z0-9_.:-]/g, '_') .slice(0, 64); const datasetScope = `${options.client.baseUrl}:${toolId}:${sql}:${datasetLimit}`; const playDataset = createDeferredPlayDataset({ datasetKind: 'csv', datasetId: `sdk-tool-list:${toolId}:${stableHash(datasetScope)}:${datasetLimit}:${safeNonce}`, count: Math.min(totalRows, datasetLimit), previewRows, residentRows: rawRows.length >= effectiveCount ? rawRows.slice(0, effectiveCount) : null, sourceLabel: 'query result rows', tableNamespace: null, resolvers: { count: async () => Math.min(totalRows, datasetLimit), at: async (index) => (await fetchPage(index, 1))[0], peek: async (limit) => collectRows(limit), materialize: async (limit) => collectRows(limit), iterate: () => ({ async *[Symbol.asyncIterator]() { const count = Math.min(totalRows, datasetLimit); for ( let offset = 0; offset < count; offset += QUERY_RESULT_DATASET_PAGE_SIZE ) { yield* await fetchPage( offset, Math.min(QUERY_RESULT_DATASET_PAGE_SIZE, count - offset), ); } }, }) as AsyncIterable>, }, }); return attachToolResultListDataset(result, { name: 'rows', path: 'toolResponse.raw.rows', dataset: playDataset, count: Math.min(totalRows, datasetLimit), }); } function toolExecutionEnvelopeToResult( fallbackToolId: string, response: ToolExecution, options?: { client: DeeplineClient; input: Record; }, ): ToolExecuteResult { const raw = response.toolResponse?.raw ?? null; const rawV2 = response.toolResponse?.rawV2; const view = response.toolResponse?.view; const meta = response.toolResponse?.meta; const metadata = isRecord(response._metadata) ? response._metadata.tool : null; const toolMetadata = isRecord(metadata) ? metadata : {}; return attachSdkQueryResultDatasetResult( fallbackToolId, createToolExecuteResult({ status: typeof response.status === 'string' ? response.status : 'completed', jobId: typeof response.job_id === 'string' ? response.job_id : undefined, result: { data: raw, ...(isRecord(meta) ? { meta } : {}), }, response: { ...(rawV2 !== undefined ? { rawV2 } : {}), ...(view === 'data' || view === 'rawV2' ? { view } : {}), ...(isRecord(meta) ? { meta } : {}), }, metadata: { toolId: typeof toolMetadata.toolId === 'string' ? toolMetadata.toolId : fallbackToolId, extractors: extractorDescriptorRecord(toolMetadata.extractors), targetGetters: stringArrayRecord(toolMetadata.targetGetters), listExtractorPaths: stringArray(toolMetadata.listExtractorPaths), listIdentityGetters: stringArrayRecord( toolMetadata.listIdentityGetters, ), }, execution: { idempotent: true, cached: false, source: 'live', }, meta: isRecord(response.meta) ? response.meta : undefined, }), options, ); } export function defineInput( schema: Record, ): PlayInputContract { return createPlayInputContract(schema); } /** * Define a play — a composable TypeScript workflow for the Deepline platform. * * The returned value is both: * 1. **A callable function** — invoked by the Temporal worker with a runtime context * 2. **A named play handle** — with `.run()`, `.versions()`, `.get()`, `.publish()`, etc. for remote lifecycle management * * Plays are the primary abstraction for building repeatable data pipelines. * They run on Temporal for durable execution with automatic retries and timeouts. * * @typeParam TInput - The input type accepted by the play * @typeParam TOutput - The return type of the play * @param config - Object-form play config. * @param name - Play name. * @param fn - Play function. * @param bindings - Play configuration, including runtime limits and triggers. * @returns A {@link DefinedPlay} that is both callable and has lifecycle methods * * @example Basic play * ```typescript * import { definePlay } from 'deepline'; * * export default definePlay('company-lookup', async (ctx, input: { domain: string }) => { * ctx.log(`Searching for ${input.domain}`); * const company = await ctx.tools.execute({ * id: 'company_search', * tool: 'test_company_search', * input: { domain: input.domain }, * description: 'Look up company details by domain.', * }); * return company; * }); * ``` * * @example CSV processing play * ```typescript * export default definePlay('bulk-enrich', async (ctx, input: { csv: string }) => { * const leads = await ctx.csv(input.csv); * ctx.log(`Processing ${await leads.count()} rows`); * const results = await ctx * .dataset('companies', leads) * .withColumn('company', (row, ctx) => * ctx.tools.execute({ * id: 'company_search', * tool: 'test_company_search', * input: { domain: row.domain }, * description: 'Look up company details by domain.', * })) * .run({ description: 'Enrich lead companies.' }); * return results; * }); * ``` * * @example With cron binding * ```typescript * export default definePlay('daily-report', async (ctx) => { * const data = await ctx.tools.execute({ * id: 'crm_export', * tool: 'crm_export', * input: { since: 'yesterday' }, * description: 'Export yesterday CRM records.', * }); * return data; * }, { * cron: { schedule: '0 9 * * *', timezone: 'America/New_York' }, * }); * ``` * * @example Programmatic lifecycle * ```typescript * const myPlay = definePlay('my-play', handler); * * // Get play definition: * const detail = await myPlay.get(); * * // Run remotely: * const result = await myPlay.runSync({ domain: 'stripe.com' }); * * // Make the current draft live: * await myPlay.publish(); * ``` */ export function definePlay( config: DefinePlayConfig, ): DefinedPlay; /** * Define a play with a name and function. * * @param name - Play name. * @param fn - Play function. * @param bindings - Play configuration, including runtime limits and triggers. * @returns Play handle. */ export function definePlay( name: string, fn: (ctx: DeeplinePlayRuntimeContext, input: TInput) => Promise, bindings?: PlayBindings, ): DefinedPlay; /** * @sdkReference runtime 010 */ export function definePlay( nameOrConfig: string | DefinePlayConfig, maybeFn?: ( ctx: DeeplinePlayRuntimeContext, input: TInput, ) => Promise, maybeBindings?: PlayBindings, ): DefinedPlay { const config = typeof nameOrConfig === 'string' ? { name: nameOrConfig, fn: maybeFn, bindings: maybeBindings, description: maybeBindings?.description, inputSchema: undefined, billing: maybeBindings?.billing, runtime: maybeBindings?.runtime, compatibility: maybeBindings?.compatibility, } : { name: nameOrConfig.id, fn: nameOrConfig.run, bindings: nameOrConfig.bindings, description: nameOrConfig.description, inputSchema: nameOrConfig.input.schema, billing: nameOrConfig.billing, runtime: nameOrConfig.runtime ?? nameOrConfig.bindings?.runtime, compatibility: nameOrConfig.compatibility ?? nameOrConfig.bindings?.compatibility, }; const name = config.name; const fn = config.fn; const bindings = config.bindings; const description = config.description?.trim(); const billing = config.billing; const inputSchema = config.inputSchema; const runtime = config.runtime; const compatibility = config.compatibility; if (typeof fn !== 'function') { throw new Error('definePlay(...) requires an async run function.'); } if (name.includes('/')) { throw new Error( 'definePlay(name, ...) play names cannot contain "/". Slash is reserved for qualified references like "prebuilt/example" or "self/example".', ); } const normalizedName = name .trim() .replace(/[^a-z0-9]+/gi, '_') .replace(/_+/g, '_') .replace(/^_+|_+$/g, '') .toLowerCase(); if (!normalizedName) { throw new Error( 'definePlay(name, ...) requires a play name with at least one letter or number. ' + 'Use only letters, numbers, underscores, or hyphens.', ); } if (normalizedName.length > 63) { throw new Error( `definePlay("${name}", ...) is too long after normalization (${normalizedName.length}/63). ` + `Shorten the play name to 63 characters or fewer. Normalized value: "${normalizedName}".`, ); } const metadata: PlayMetadata = { name, ...(description ? { description } : {}), ...(bindings ? { bindings } : {}), ...(inputSchema ? { inputSchema } : {}), ...(billing ? { billing } : {}), ...(runtime ? { runtime } : {}), ...(compatibility ? { compatibility } : {}), }; const play = fn as DefinedPlay; Object.defineProperty(play, PLAY_METADATA_SYMBOL, { value: metadata, enumerable: false, configurable: false, writable: false, }); Object.defineProperty(play, 'playName', { value: name, enumerable: true, configurable: false, writable: false, }); Object.defineProperty(play, 'bindings', { value: bindings, enumerable: true, configurable: false, writable: false, }); Object.defineProperty(play, 'runtime', { value: runtime, enumerable: true, configurable: false, writable: false, }); Object.defineProperty(play, 'compatibility', { value: compatibility, enumerable: true, configurable: false, writable: false, }); const handle = createNamedPlayHandle( () => new DeeplineClient(), name, ); for (const key of [ 'name', 'get', 'runs', 'versions', 'publish', 'run', 'runSync', ] as const) { Object.defineProperty(play, key, { value: handle[key], enumerable: false, configurable: false, writable: false, }); } return play; } /** * Alias for {@link definePlay}. Workflows and plays share the same public * Deepline SDK contract; the selected execution profile decides whether the * run is backed by local Node, Temporal/Daytona, or Cloudflare Dynamic * Workflows. */ export const defineWorkflow = definePlay; /** * Extract play metadata from a value that may be a defined play. * * Used internally by the CLI and bundler to detect `definePlay()` exports * and extract the play name and bindings. * * @param value - Any value (typically a module's default export) * @returns Play metadata if the value is a defined play, `null` otherwise * * @example * ```typescript * import { getDefinedPlayMetadata } from 'deepline'; * * const mod = await import('./my-play.play.ts'); * const meta = getDefinedPlayMetadata(mod.default); * if (meta) { * console.log(`Play name: ${meta.name}`); * console.log(`Bindings:`, meta.bindings); * } * ``` */ export function getDefinedPlayMetadata(value: unknown): PlayMetadata | null { if (typeof value !== 'function') { return null; } const metadata = (value as unknown as Record)[ PLAY_METADATA_SYMBOL ]; if (!metadata || typeof metadata !== 'object') { return null; } const candidate = metadata as PlayMetadata; if (!candidate.name || typeof candidate.name !== 'string') { return null; } return candidate; }