Skip to main content

Raw Protocol

Write handlers in any language without an SDK using the raw stdin/stdout protocol.

Protocol Overview

TaskDaemon communicates with handlers via:
  • Input: JSON task objects sent to stdin (one per line)
  • Output: JSON result objects written to stdout (one per line)
The handler process stays alive and processes multiple tasks.

Input Format

Each line on stdin is a JSON object:
{
  "task_id": "550e8400-e29b-41d4-a716-446655440000",
  "task_type": "process",
  "task_data": {"key": "value"},
  "attempt": 0
}
FieldTypeDescription
task_idstringUnique task identifier (UUID)
task_typestringHandler type from config
task_dataobjectArbitrary input data from client
attemptnumberCurrent attempt number (0-indexed)

Output Format

Write one JSON object per line to stdout.

Success Response

{"status": "success", "result": {"key": "value"}}

Error Response

{"status": "error", "error": "Error message", "retryable": false}
FieldTypeDescription
statusstring"success" or "error"
resultanyResult data (success only)
errorstringError message (error only)
retryablebooleanWhether to retry (error only, default: false)

Example Implementations

Python (No SDK)

import json
import sys

for line in sys.stdin:
    task = json.loads(line)
    
    try:
        # Process task
        name = task["task_data"].get("name", "World")
        result = {"message": f"Hello, {name}!"}
        response = {"status": "success", "result": result}
    except Exception as e:
        response = {"status": "error", "error": str(e), "retryable": False}
    
    print(json.dumps(response), flush=True)

Node.js (No SDK)

const readline = require('readline');

const rl = readline.createInterface({ input: process.stdin });

rl.on('line', (line) => {
  const task = JSON.parse(line);
  
  try {
    const name = task.task_data.name || 'World';
    const result = { message: `Hello, ${name}!` };
    console.log(JSON.stringify({ status: 'success', result }));
  } catch (e) {
    console.log(JSON.stringify({ status: 'error', error: String(e), retryable: false }));
  }
});

Ruby

require 'json'

STDOUT.sync = true

STDIN.each_line do |line|
  task = JSON.parse(line)
  
  begin
    name = task['task_data']['name'] || 'World'
    result = { message: "Hello, #{name}!" }
    puts JSON.generate({ status: 'success', result: result })
  rescue => e
    puts JSON.generate({ status: 'error', error: e.message, retryable: false })
  end
end

PHP

<?php
while ($line = fgets(STDIN)) {
    $task = json_decode($line, true);
    
    try {
        $name = $task['task_data']['name'] ?? 'World';
        $result = ['message' => "Hello, {$name}!"];
        $response = ['status' => 'success', 'result' => $result];
    } catch (Exception $e) {
        $response = ['status' => 'error', 'error' => $e->getMessage(), 'retryable' => false];
    }
    
    echo json_encode($response) . "\n";
    flush();
}

Bash

#!/bin/bash
while IFS= read -r line; do
    name=$(echo "$line" | jq -r '.task_data.name // "World"')
    echo "{\"status\": \"success\", \"result\": {\"message\": \"Hello, $name!\"}}"
done

Perl

use JSON;
use strict;

$| = 1;  # Autoflush

while (my $line = <STDIN>) {
    my $task = decode_json($line);
    my $name = $task->{task_data}{name} // 'World';
    
    print encode_json({
        status => 'success',
        result => { message => "Hello, $name!" }
    }) . "\n";
}

Important Requirements

1. Flush Output

Always flush stdout after writing to ensure TaskDaemon receives the response immediately:
print(json.dumps(response), flush=True)  # Python
// Node.js console.log auto-flushes
console.log(JSON.stringify(response));
STDOUT.sync = true  # Ruby

2. One Response Per Task

Write exactly one JSON line per input line. Don’t write partial responses or multiple lines.

3. Keep Running

The handler process stays alive and processes multiple tasks. Don’t exit after one task:
# Wrong - exits after one task
task = json.loads(input())
print(json.dumps(result))

# Correct - keeps running
for line in sys.stdin:
    task = json.loads(line)
    print(json.dumps(result), flush=True)

4. Handle Errors Gracefully

Return error status instead of crashing:
try:
    result = process(task)
    response = {"status": "success", "result": result}
except Exception as e:
    response = {"status": "error", "error": str(e), "retryable": False}

5. Use Unbuffered I/O

Configure your runtime for unbuffered or line-buffered I/O:
# Python
ENV PYTHONUNBUFFERED=1
CMD ["python", "-u", "handler.py"]

# Node.js (auto-flushes)
CMD ["node", "handler.js"]

# Ruby
CMD ["ruby", "-e", "$stdout.sync=true; load 'handler.rb'"]

Dockerfile Template

FROM your-base-image

COPY handler.ext /app/
WORKDIR /app

# Ensure unbuffered output (language-specific)
ENV PYTHONUNBUFFERED=1

CMD ["your-runtime", "handler.ext"]

Testing Handlers Locally

Test your handler without TaskDaemon:
echo '{"task_id":"test","task_type":"greet","task_data":{"name":"World"},"attempt":0}' | ./handler
Expected output:
{"status":"success","result":{"message":"Hello, World!"}}