Skip to main content

Synchronous Webhooks

AutoKitteh webhooks can operate in two modes: asynchronous (default) and synchronous. This page explains how to configure and use synchronous webhooks to return HTTP responses from your workflows.

Asynchronous vs. Synchronous

Asynchronous Webhooks (Default)

By default, webhook triggers return an immediate 202 Accepted response and execute the workflow in the background.

triggers:
- name: webhook
type: webhook
call: program.py:handler
# is_sync: false (default)

Behavior:

  1. HTTP request arrives
  2. AutoKitteh returns 202 Accepted immediately
  3. Workflow session starts and runs independently
  4. Client receives response before workflow completes

Use cases:

  • Long-running workflows
  • Fire-and-forget operations
  • Webhooks from services that don't need responses
  • Background processing

Synchronous Webhooks

With is_sync: true, the webhook waits for the workflow to produce a response before replying to the HTTP client.

triggers:
- name: api_endpoint
type: webhook
is_sync: true
call: program.py:api_handler

Behavior:

  1. HTTP request arrives
  2. Workflow session starts
  3. AutoKitteh streams session outcomes
  4. When workflow calls http_outcome(more=False), that outcome becomes the HTTP response
  5. Client receives the response
  6. Workflow can continue executing after sending response

Use cases:

  • REST APIs
  • Webhooks that require immediate response
  • Request/response patterns
  • Validation endpoints

Basic Example

Configuration

version: v2

project:
name: simple_api

triggers:
- name: api
type: webhook
is_sync: true
call: program.py:handle_request

Code

import json
import autokitteh

def handle_request(event):
# Parse request
data = json.loads(event.data.body.text)

# Process
result = {"message": f"Hello, {data['name']}!"}

# Send response
autokitteh.http_outcome(
status_code=200,
headers={"Content-Type": "application/json"},
body=json.dumps(result)
)

Request/Response

$ curl -X POST http://localhost:9980/webhooks/abc123 \
-H "Content-Type: application/json" \
-d '{"name": "Alice"}'

{"message": "Hello, Alice!"}

The http_outcome() Function

Signature

autokitteh.http_outcome(
status_code: int = 200,
headers: dict = None,
body: str = None,
more: bool = False
) -> None

Parameters

  • status_code (int): HTTP status code (default: 200)
  • headers (dict): Response headers as key-value pairs
  • body (str): Response body content
  • more (bool): If True, workflow continues sending outcomes. If False (default), this outcome becomes the final HTTP response

Examples

Simple text response:

autokitteh.http_outcome(
status_code=200,
body="Success"
)

JSON response:

import json

autokitteh.http_outcome(
status_code=200,
headers={"Content-Type": "application/json"},
body=json.dumps({"status": "ok", "data": result})
)

Error response:

autokitteh.http_outcome(
status_code=400,
headers={"Content-Type": "application/json"},
body=json.dumps({"error": "Invalid input"})
)

Custom headers:

autokitteh.http_outcome(
status_code=201,
headers={
"Content-Type": "application/json",
"X-Request-ID": request_id,
"Location": f"/api/resources/{resource_id}"
},
body=json.dumps({"id": resource_id})
)

Streaming Outcomes

The more parameter allows workflows to send multiple outcomes before the final response.

def handle_request(event):
# Send intermediate outcome (more=True)
autokitteh.http_outcome(
status_code=200,
body="Processing...",
more=True
)

# Do work
result = long_running_operation()

# Send final outcome (more=False, default)
autokitteh.http_outcome(
status_code=200,
body=f"Result: {result}"
)

# Workflow can continue after response is sent
log_completion(result)
note

Only the last outcome with more=False becomes the HTTP response. Intermediate outcomes are logged but not sent to the client.

Continue After Response

Workflows can continue executing after sending the HTTP response. This is useful for:

  • Logging and cleanup
  • Triggering follow-up actions
  • Updating metrics
  • Background processing
def handle_order(event):
order = validate_order(event.data)

# Send immediate response to client
autokitteh.http_outcome(
status_code=202,
headers={"Content-Type": "application/json"},
body=json.dumps({"order_id": order.id, "status": "accepted"})
)

# Continue processing after response sent
process_payment(order)
update_inventory(order)
send_confirmation_email(order)
notify_warehouse(order)

Advanced Examples

REST API with Validation

import json
import autokitteh

def api_create_user(event):
try:
# Parse request
data = json.loads(event.data.body.text)

# Validate
if not data.get("email") or not data.get("name"):
autokitteh.http_outcome(
status_code=400,
headers={"Content-Type": "application/json"},
body=json.dumps({"error": "Missing required fields"})
)
return

# Create user
user = create_user(data["email"], data["name"])

# Success response
autokitteh.http_outcome(
status_code=201,
headers={
"Content-Type": "application/json",
"Location": f"/api/users/{user.id}"
},
body=json.dumps({
"id": user.id,
"email": user.email,
"name": user.name
})
)

except json.JSONDecodeError:
autokitteh.http_outcome(
status_code=400,
headers={"Content-Type": "application/json"},
body=json.dumps({"error": "Invalid JSON"})
)
except Exception as e:
autokitteh.http_outcome(
status_code=500,
headers={"Content-Type": "application/json"},
body=json.dumps({"error": "Internal server error"})
)

Webhook with Signature Verification

import hmac
import hashlib
import json
import autokitteh

SECRET = os.getenv("WEBHOOK_SECRET")

def verify_webhook(event):
# Get signature from headers
signature = event.data.headers.get("X-Hub-Signature-256", "")
body = event.data.body.bytes

# Verify signature
expected = "sha256=" + hmac.new(
SECRET.encode(),
body,
hashlib.sha256
).hexdigest()

if not hmac.compare_digest(signature, expected):
autokitteh.http_outcome(
status_code=401,
body="Invalid signature"
)
return None

# Parse and return payload
return json.loads(body)

def handle_webhook(event):
data = verify_webhook(event)
if not data:
return # Response already sent

# Process webhook
process_event(data)

# Send success response
autokitteh.http_outcome(
status_code=200,
body="OK"
)

Waiting for Events

Synchronous webhooks can wait for other events before responding:

triggers:
- name: first
type: webhook
is_sync: true
call: program.py:on_first

- name: second
type: webhook
# No call - just allocates a webhook URL
from datetime import timedelta
from autokitteh import http_outcome, next_event, subscribe

def on_first(event):
print("First webhook triggered, waiting for second...")

# Subscribe to second webhook
sub = subscribe("second")

# Wait up to 1 minute
second_event = next_event(sub, timeout=timedelta(minutes=1))

if second_event:
# Send response with data from second webhook
http_outcome(
status_code=200,
body=f"Got second webhook: {second_event.data.body.text}"
)
else:
# Timeout
http_outcome(
status_code=408,
body="Timeout waiting for second webhook"
)

See: kittehub/samples/sync_webhook

Performance Considerations

Response Time

Synchronous webhooks block the HTTP connection until the workflow sends a response. Keep workflows fast:

  • ✅ Validate and respond quickly
  • ✅ Use non-durable mode for speed (see below)
  • ✅ Move heavy processing after the response
  • ⚠️ Avoid long database queries before response
  • ⚠️ Avoid waiting for external APIs before response

Durability Mode

For synchronous webhooks, consider using non-durable mode for better performance:

triggers:
- name: api
type: webhook
is_sync: true
is_durable: false # Faster, recommended for APIs
call: program.py:handler

Durable mode (is_durable: true) adds overhead and may cause timeouts during replay. Use only if:

  • Response time is not critical
  • Workflow has critical operations before response
  • You need state persistence during the response phase

See: Durability Modes

Timeouts

  • HTTP clients typically timeout after 30-60 seconds
  • Keep response generation under 10 seconds for good user experience
  • Use more=False to send response quickly
  • Continue heavy work after response is sent

Error Handling

Always handle errors and send appropriate HTTP responses:

def handle_request(event):
try:
# Your logic here
result = process(event.data)

autokitteh.http_outcome(
status_code=200,
body=json.dumps({"result": result})
)

except ValueError as e:
# Client error
autokitteh.http_outcome(
status_code=400,
body=json.dumps({"error": str(e)})
)

except Exception as e:
# Server error
print(f"Error: {e}")
autokitteh.http_outcome(
status_code=500,
body=json.dumps({"error": "Internal server error"})
)

Comparison: Async vs. Sync

FeatureAsync (is_sync: false)Sync (is_sync: true)
HTTP ResponseImmediate 202From workflow
Response Time< 1msDepends on workflow
DurabilityRecommendedUse with caution
Use CaseBackground jobsAPIs, webhooks
Client WaitsNoYes
Custom StatusNoYes
Custom HeadersNoYes
Custom BodyNoYes

Best Practices

  1. Use async by default - only enable sync when needed
  2. Respond quickly - send response within seconds
  3. Validate early - check inputs before heavy processing
  4. Use non-durable mode - for better API performance
  5. Handle errors - always send appropriate status codes
  6. Set Content-Type - specify the response format
  7. Continue after response - move heavy work after http_outcome()
  8. Test timeouts - ensure workflows respond before client timeout
  9. Log outcomes - track responses for debugging
  10. Document your API - specify expected request/response formats

Limitations

  • Only works with type: webhook triggers
  • Cannot change response after http_outcome(more=False) is called
  • HTTP client must support waiting for response
  • Some webhook providers expect immediate 200 OK (use async mode)

See Also