Quick Start

Get up and running with Cyberdesk in under 5 minutes. This guide assumes you’ve already created workflows in the Cyberdesk Dashboard.
1

Install the SDK

npm install cyberdesk
2

Initialize the client and create a run

import { createCyberdeskClient } from 'cyberdesk';

// Initialize the client
const client = createCyberdeskClient('YOUR_API_KEY');

// Create a run for your workflow
const { data: run } = await client.runs.create({
  workflow_id: 'your-workflow-id',
  machine_id: 'your-machine-id',
  input_values: {
    patient_id: '12345',
    patient_first_name: 'John',
    patient_last_name: 'Doe'
  }
});

// Wait for the run to complete
let status = run.status;
while (status === 'scheduling' || status === 'running') {
  await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
  const { data: updatedRun } = await client.runs.get(run.id);
  status = updatedRun.status;
}

// Get the output data
if (status === 'success') {
  console.log('Patient data:', updatedRun.output_data);
} else {
  console.error('Run failed:', updatedRun.error?.join(', '));
}
We recommend creating and managing workflows through the Cyberdesk Dashboard. The dashboard editor supports rich, multimodal prompts — you can add screenshots or UI snippets directly into your prompt to guide the agent. The SDK is optimized for executing runs against your existing workflows.

Installation & Setup

Prerequisites

  • Node.js 14.0 or higher
  • TypeScript 4.0 or higher (for TypeScript projects)

Installation

npm install cyberdesk

TypeScript Configuration

The SDK includes TypeScript definitions out of the box. For the best experience, ensure your tsconfig.json includes:
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Authentication

Creating a Client

import { createCyberdeskClient } from 'cyberdesk';

const client = createCyberdeskClient('YOUR_API_KEY');

Custom Base URL

For self-hosted or enterprise deployments:
const client = createCyberdeskClient('YOUR_API_KEY', 'https://api.your-domain.com');
Never hardcode API keys in your source code. Use environment variables:
const client = createCyberdeskClient(process.env.CYBERDESK_API_KEY!);

Working with Runs

Runs are the primary way to execute workflows in Cyberdesk. Here’s everything you need to know about managing runs through the SDK.

Creating a Run

const { data: run, error } = await client.runs.create({
  workflow_id: 'workflow-uuid',
  machine_id: 'machine-uuid',
  input_values: {
    // Your workflow-specific input data
    patient_id: '12345',
    patient_first_name: 'John',
    patient_last_name: 'Doe'
  }
});

if (error) {
  console.error('Failed to create run:', error);
} else {
  console.log('Run created:', run.id);
}

Creating a Run with Machine Pools

You can specify pool requirements when creating a run. This ensures your run is executed on a machine that belongs to ALL specified pools. This is especially useful for:
  • Running workflows on customer-specific machines
  • Requiring machines with specific software installed
  • Organizing machines by location or capability
// Get pool IDs (typically from your configuration or database)
const customerPoolId = 'pool-uuid-1';  // e.g., "Customer A" pool
const excelPoolId = 'pool-uuid-2';     // e.g., "Has Excel" pool

const { data: run, error } = await client.runs.create({
  workflow_id: 'workflow-uuid',
  // Machine must be in BOTH pools (intersection, not union)
  pool_ids: [customerPoolId, excelPoolId],
  input_values: {
    patient_id: '12345',
    patient_first_name: 'John',
    patient_last_name: 'Doe'
  }
});

if (error) {
  console.error('Failed to create run:', error);
} else {
  console.log('Run created:', run.id);
  console.log('Will execute on machine in pools:', [customerPoolId, excelPoolId]);
}
Pool Matching Logic: When you specify multiple pools, Cyberdesk will only select machines that belong to ALL specified pools (intersection). For example, if you specify ["Customer A", "Has Excel"], only machines that are in both pools will be considered.
If you provide a machine_id when creating a run, pool_ids are ignored. Cyberdesk will only attempt the specified machine; if it’s busy or unavailable, the run will wait until that machine is free (no fallback to other machines or pools).
Creating and Managing Pools: While you can manage pools via the SDK, we recommend using the Cyberdesk Dashboard for a more intuitive experience:
  1. Navigate to any machine in the dashboard
  2. Click on the machine to view its details
  3. Add the machine to existing pools or create new pools
  4. Assign multiple pools to organize machines by customer, capability, or location
Common pool strategies:
  • By Customer: “Customer A”, “Customer B”, etc.
  • By Software: “Has Excel”, “Has Chrome”, “Has Epic EHR”
  • By Environment: “Production”, “Staging”, “Development”
  • By Location: “US-East”, “EU-West”, etc.

Creating a Run with File Inputs

You can attach files to a run at creation. This is useful for workflows that need to process or manipulate files on the remote machine.
import { promises as fs } from 'fs';
import type { FileInput } from 'cyberdesk';

// Read a file and convert it to base64
const fileBuffer = await fs.readFile('path/to/your/file.txt');
const content = fileBuffer.toString('base64');

const fileInputs: FileInput[] = [
  {
    filename: 'file.txt',
    content: content,
    target_path: 'C:/Users/Default/Desktop/file.txt', // Optional
    cleanup_imports_after_run: true // Optional
  }
];

const { data: run, error } = await client.runs.create({
  workflow_id: 'workflow-uuid',
  file_inputs: fileInputs
});

if (error) {
  console.error('Failed to create run:', error);
} else {
  console.log('Run created with file attachment:', run.id);
}
filename
string
required
The name of the file, including its extension.
content
string
required
The base64-encoded content of the file.
target_path
string
The absolute path on the remote machine where the file should be saved. If not provided, it defaults to ~/CyberdeskTransfers/.
cleanup_imports_after_run
boolean
If true, the file will be deleted from the remote machine after the run completes (whether it succeeds or fails). Defaults to false.

Listing Runs

// List all runs
const { data: runs } = await client.runs.list();

// List with pagination
const { data: paginatedRuns } = await client.runs.list({
  skip: 0,
  limit: 20
});

// Filter by status
const { data: completedRuns } = await client.runs.list({
  status: 'success'
});

// Filter by workflow
const { data: workflowRuns } = await client.runs.list({
  workflow_id: 'workflow-uuid'
});

Getting a Specific Run

const { data: run, error } = await client.runs.get('run-uuid');

if (run) {
  console.log('Run status:', run.status);
  console.log('Output data:', run.output_data);
}

Updating a Run

Run updates are typically handled automatically by the Cyberdesk system. Manual updates are rarely needed.
const { data: updatedRun } = await client.runs.update('run-uuid', {
  status: 'cancelled'
});

Deleting a Run

const { error } = await client.runs.delete('run-uuid');

if (!error) {
  console.log('Run deleted successfully');
}

Polling for Run Completion

Here’s a robust pattern for waiting for runs to complete:
async function waitForRunCompletion(client: any, runId: string, timeoutMs = 300000) {
  const startTime = Date.now();
  const pollInterval = 5000; // 5 seconds

  while (Date.now() - startTime < timeoutMs) {
    const { data: run, error } = await client.runs.get(runId);
    
    if (error) {
      throw new Error(`Failed to get run status: ${error}`);
    }

    if (run.status === 'success') {
      return run;
    }
    
    if (run.status === 'error' || run.status === 'cancelled') {
      throw new Error(`Run ${run.status}: ${run.error?.join(', ') || 'Unknown error'}`);
    }

    await new Promise(resolve => setTimeout(resolve, pollInterval));
  }

  throw new Error('Run timed out');
}

// Usage
try {
  const completedRun = await waitForRunCompletion(client, run.id);
  console.log('Output:', completedRun.output_data);
} catch (error) {
  console.error('Run failed:', error);
}

Working with File Attachments

Manage files associated with your runs, such as input files uploaded at creation or output files generated by a workflow.

Listing Run Attachments

You can list all attachments for a specific run and filter them by type (input or output).
// List all attachments for a run
const { data: attachments } = await client.run_attachments.list({
  run_id: 'run-uuid'
});

// List only output attachments
const { data: outputFiles } = await client.run_attachments.list({
  run_id: 'run-uuid',
  attachment_type: 'output'
});

Downloading an Attachment

There are two ways to download attachments depending on your use case:

Method 1: Get a Download URL

Get a signed URL that triggers automatic download when accessed. Perfect for web applications where you want to provide download links to users.
// Get a download URL with custom expiration (default: 5 minutes)
const { data } = await client.run_attachments.getDownloadUrl(
  'attachment-uuid',
  600  // 10 minutes
);

if (data) {
  console.log(`Download URL: ${data.url}`);
  console.log(`Expires in: ${data.expires_in} seconds`);
  
  // You can use this URL in your web app
  // For example, in a React component:
  // <a href={data.url} download>Download File</a>
}

Method 2: Download Raw File Content

Download the file content directly. The SDK will return the raw data which you can then save to a file or process further.
import { promises as fs } from 'fs';

// Get the attachment metadata first
const { data: attachmentInfo } = await client.run_attachments.get('attachment-uuid');

if (attachmentInfo) {
  // Download the file content
  const { data: fileData, error } = await client.run_attachments.download(attachmentInfo.id);

  if (fileData) {
    // For Node.js: Save to file
    const buffer = Buffer.from(fileData);
    await fs.writeFile(attachmentInfo.filename, buffer);
    console.log(`Downloaded ${attachmentInfo.filename}`);
    
    // For browsers: Create a Blob and download
    // const blob = new Blob([fileData]);
    // const url = URL.createObjectURL(blob);
    // const a = document.createElement('a');
    // a.href = url;
    // a.download = attachmentInfo.filename;
    // a.click();
  }
}

Example: Upload, Process, and Download

Here’s a full example of a workflow that processes a file.
  1. Workflow Prompt: "Take the file at ~/CyberdeskTransfers/report.txt, add a summary to the end of it, and mark it for export."
  2. Workflow Setting: includes_file_exports is set to true.
import { createCyberdeskClient, FileInput } from 'cyberdesk';
import { promises as fs } from 'fs';

async function main() {
  const client = createCyberdeskClient('YOUR_API_KEY');

  // 1. Prepare and upload the input file
  const reportContent = "This is the initial report content.";
  const encodedContent = Buffer.from(reportContent).toString('base64');

  const { data: run } = await client.runs.create({
    workflow_id: "your-file-processing-workflow-id",
    file_inputs: [{ filename: "report.txt", content: encodedContent }]
  });
  console.log(`Run started: ${run.id}`);

  // 2. Wait for the run to complete
  const completedRun = await waitForRunCompletion(client, run.id);
  console.log(`Run finished with status: ${completedRun.status}`);

  // 3. Find and download the output attachment
  if (completedRun.status === 'success') {
    const { data: outputAttachments } = await client.run_attachments.list({
      run_id: completedRun.id,
      attachment_type: 'output'
    });
    
    if (outputAttachments?.items?.length) {
      const processedReport = outputAttachments.items[0];
      
      // Option 1: Get a download URL (for web apps)
      const { data: urlData } = await client.run_attachments.getDownloadUrl(processedReport.id);
      if (urlData) {
        console.log(`Download URL: ${urlData.url}`);
        console.log(`Valid for: ${urlData.expires_in} seconds`);
      }
      
      // Option 2: Download the processed file directly
      const { data: fileData } = await client.run_attachments.download(processedReport.id);
      
      if (fileData) {
        const processedContent = new TextDecoder().decode(fileData);
        console.log("\n--- Processed Report ---");
        console.log(processedContent);
        console.log("------------------------");
      }
    } else {
      console.log("No output files were generated.");
    }
  }
}

// Assuming waitForRunCompletion is defined as in the previous examples
main();
This example demonstrates the complete lifecycle: uploading a file with a run, executing a workflow that modifies it, and then retrieving the processed file from the run’s output attachments.

Bulk Creating Runs with Pools

When creating multiple runs in bulk, you can also specify pool requirements. All runs will be distributed across machines that match the pool criteria.
// Create 100 runs that require machines in specific pools
const { data: result, error } = await client.runs.bulkCreate({
  workflow_id: 'workflow-uuid',
  count: 100,
  pool_ids: ['customer-a-pool-id', 'excel-pool-id'],
  input_values: {
    task_type: 'data_extraction',
    priority: 'high'
  }
});

if (result) {
  console.log(`Created ${result.created_runs.length} runs`);
  console.log(`Failed: ${result.failed_count}`);
  // All runs will execute on machines in both specified pools
}
Bulk Run Assignment: When bulk creating runs with pool requirements, Cyberdesk attempts to assign each run to any available machine that meets the pool criteria. If no matching machine is available, runs remain in scheduling until one is free. No specific load balancing guarantees are made.
If you provide a machine_id in a bulk run request, pool_ids are ignored for those runs. Each run will only target the specified machine; if it is busy, the run will wait for that machine rather than falling back to other machines or pools.

Real-World Example: Healthcare Integration

Here’s a complete example of retrieving patient data from an Epic EHR system using Cyberdesk:
import { createCyberdeskClient } from 'cyberdesk';

async function getPatientData(patientId: string, firstName: string, lastName: string) {
  const client = createCyberdeskClient(process.env.CYBERDESK_API_KEY!);

  try {
    // Create a run to fetch patient data
    const { data: run, error } = await client.runs.create({
      workflow_id: '550e8400-e29b-41d4-a716-446655440000',  // Your Epic workflow ID
      machine_id: '550e8400-e29b-41d4-a716-446655440001',   // Your Epic machine ID
      input_data: {
        patient_id: patientId,
        patient_first_name: firstName,
        patient_last_name: lastName
      }
    });

    if (error) {
      throw new Error(`Failed to create run: ${error}`);
    }

    console.log(`Fetching data for patient ${firstName} ${lastName} (${patientId})...`);

    // Wait for completion
    const completedRun = await waitForRunCompletion(client, run.id, 120000); // 2 minute timeout

    // Process the patient data
    const patientData = completedRun.output_data;
    
    return {
      patientId: patientId,
      demographics: patientData.demographics,
      medications: patientData.medications,
      vitals: patientData.recentVitals,
      lastUpdated: patientData.lastUpdated
    };

  } catch (error) {
    console.error('Error fetching patient data:', error);
    throw error;
  }
}

// Express.js route example
app.post('/api/patients/lookup', async (req, res) => {
  try {
    const { patient_id, first_name, last_name } = req.body;
    const patientData = await getPatientData(patient_id, first_name, last_name);
    res.json(patientData);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch patient data' });
  }
});

Other SDK Resources

Important: While the SDK provides full CRUD operations for all Cyberdesk resources, we strongly recommend using the Cyberdesk Dashboard for managing these resources. The dashboard provides a more intuitive interface for:
  • Creating and editing workflows
  • Managing machines
  • Viewing connections
  • Analyzing trajectories
The SDK methods below are provided for advanced use cases and automation scenarios.

Error Handling

All SDK methods return an object with data and error properties:
const { data, error } = await client.runs.create({
  workflow_id: 'workflow-id',
  machine_id: 'machine-id'
});

if (error) {
  // Handle error
  console.error('Error details:', error);
} else {
  // Use data
  console.log('Run created:', data.id);
}

Common Error Types

ValidationError
object
Invalid input parameters
{
  error: {
    message: "Validation failed",
    details: {
      workflow_id: "Invalid UUID format"
    }
  }
}
AuthenticationError
object
Invalid or missing API key
{
  error: {
    message: "Authentication failed",
    status: 401
  }
}
RateLimitError
object
Too many requests
{
  error: {
    message: "Rate limit exceeded",
    status: 429,
    retryAfter: 60
  }
}

TypeScript Types

The SDK exports all types for better IDE support:
import type {
  MachineResponse,
  PoolResponse,
  PoolCreate,
  PoolUpdate,
  MachinePoolUpdate,
  WorkflowResponse,
  RunResponse,
  RunStatus,
  MachineStatus,
  ConnectionStatus
} from 'cyberdesk';

// Use types in your code
function handleRun(run: RunResponse) {
  if (run.status === 'success') {
    // TypeScript knows output_data exists
    console.log(run.output_data);
  }
}

Best Practices

Use Environment Variables

Store API keys and workflow IDs in environment variables, never in code.

Implement Retry Logic

Add exponential backoff for transient failures and rate limits.

Handle Timeouts

Set reasonable timeouts for run completion based on your workflow complexity.

Log Everything

Keep detailed logs of run IDs and statuses for debugging and audit trails.

Next Steps