Struckdown Playground
The Struckdown Playground is a web-based editor for creating and testing struckdown prompts interactively.
It provides real-time syntax validation, dynamic input fields, batch processing, and instant output rendering.
Quick Start (Local Mode)
# Edit a new or existing file
sd edit myfile.sd
# With no path, opens a workspace browser on the current directory
sd edit
# Use a specific port
sd edit -p 8080
# Include custom actions/types from a directory
sd edit -I ./custom-actions myfile.sd
# Don't auto-open browser
sd edit --no-browser
The playground opens in your default browser at http://localhost:9000 (or next available port).
Features
Editor
- Syntax validation – Real-time parsing with error messages showing line/column
- Debounced analysis – 500ms delay before re-analysing to avoid excessive requests
- Keyboard shortcuts – Ctrl+S / Cmd+S triggers Save & Run
Inputs Panel
- Dynamic input fields – Automatically detects `` in your prompt
- Single/Batch toggle – Switch between individual inputs and batch file upload
- Persistent values – Input values saved to localStorage, restored on reload
- Session isolation – Each browser tab gets a unique session ID in the URL hash
Outputs Panel
- Slot-based display – Each
[[slot]]shown as a card in template order - Pin to top – Pin important outputs to keep them visible
- Cost tracking – Shows token count and estimated cost
- Copy to clipboard – One-click copy for any output
Batch Mode
- File upload – Upload xlsx or csv files
- Incremental results – Rows complete and display as they finish (SSE streaming)
- Download results – Export completed batch as xlsx
CLI Options
sd edit [PATH] [OPTIONS]
Arguments:
PATH File or directory to edit (default: the current directory, as a workspace)
Options:
-p, --port INTEGER Port to run server on (default: auto 9000+)
--no-browser Don't open browser automatically
-I, --include PATH Additional include paths for actions and types
-r, --reload Auto-reload the server on file changes (development)
-m, --models TEXT Comma-separated list of allowed models
--help Show help message
Local vs Remote Mode
| Aspect | Local Mode | Remote Mode |
|---|---|---|
| File access | Reads/writes local .sd file | No file access |
| State | File on disk | URL-encoded state |
| API credentials | From environment (LLM_API_KEY) | User enters in UI (or --api-key flag) |
| Custom actions | Loaded from -I paths and cwd | Built-in actions only |
| URL | http://localhost:9000/ | https://your-domain.com/e/{encoded} |
Deploying Remote Mode
Remote mode allows hosting the playground as a public web service where users can create and share prompts without local file access.
Important: In remote mode, API keys are NOT read from environment variables by default. This ensures that server credentials are never accidentally exposed. Users must either:
- Enter their own API key in the settings panel, or
- The server operator provides a key via
--api-key
Quick Start (Development)
# Start remote mode server (users must provide their own API keys)
sd serve
# Provide a server-side API key (for internal deployments)
sd serve --api-key=$MY_API_KEY
# Use specific port
sd serve -p 9000
# Bind to localhost only (for reverse proxy)
sd serve -h 127.0.0.1 -p 8000
Basic Deployment (Python)
# server.py
import os
from struckdown.playground import create_app
app = create_app(
prompt_file=None, # No local file
include_paths=[], # No custom actions
remote_mode=True, # Enable remote mode
# Optional: provide server-side API key
# server_api_key=os.environ.get("LLM_API_KEY"),
)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
Production Deployment with Gunicorn
# Install gunicorn
pip install gunicorn
# Run with multiple workers
gunicorn -w 4 -b 0.0.0.0:8000 "server:app"
Docker Deployment
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
# Install struckdown
RUN pip install struckdown gunicorn
# Create server script
COPY server.py .
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "server:app"]
# docker-compose.yml
version: "3.8"
services:
playground:
build: .
ports:
- "8000:8000"
restart: unless-stopped
URL Sharing (Remote Mode)
In remote mode, users can share their prompts via URL:
- Create a prompt in the editor
- The state is encoded in the URL path:
/e/{base64_compressed_state} - Share the URL – recipients see the exact same prompt and inputs
The encoding uses zlib compression + base64url, keeping URLs reasonably short.
Security Considerations
For Remote/Public Deployments
1. Reverse Proxy with HTTPS
Always deploy behind a reverse proxy (nginx, Caddy) with TLS:
# nginx.conf
server {
listen 443 ssl http2;
server_name playground.example.com;
ssl_certificate /etc/ssl/certs/cert.pem;
ssl_certificate_key /etc/ssl/private/key.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE support for batch streaming
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
}
}
2. Rate Limiting
Remote mode applies rate limits itself, with Flask-Limiter. Adjust them with the STRUCKDOWN_RATE_LIMIT, STRUCKDOWN_UPLOAD_RATE_LIMIT, STRUCKDOWN_PROMPT_RATE_LIMIT and STRUCKDOWN_PROMPT_LOAD_RATE_LIMIT environment variables (Flask-Limiter format, e.g. 100/hour). The limits are held in memory, so each worker process counts separately.
3. Request Size Limits
Uploads are capped at STRUCKDOWN_MAX_UPLOAD_SIZE bytes (5 MB by default) and templates at STRUCKDOWN_MAX_SYNTAX_LENGTH characters. See Environment Variables.
4. Action Restrictions
In remote mode, consider restricting which actions are available:
The playground already enforces this: an action is available in remote mode only if it was registered with allow_remote_use=True (the default), and the playground refuses a template that uses one that was not.
from struckdown.actions import Actions
Actions.get_remote_allowed_actions() # ['evidence', 'history', 'markdownify',
# 'noop', 'set', 'timestamp']
Actions.is_allowed_remote("fetch") # False
Register a custom action the same way when it should not run on a public deployment:
from struckdown.actions import Actions
@Actions.register("internal_lookup", allow_remote_use=False)
def internal_lookup(context, key: str):
...
6. API Key Handling
- API keys entered by users are stored only in browser localStorage
- Keys are sent per-request, never stored server-side
- Keys are never logged
7. CORS (if needed)
from flask_cors import CORS
app = create_app(remote_mode=True)
CORS(app, origins=["https://your-frontend.com"])
Recommended Production Stack
Internet
│
▼
┌─────────────┐
│ Caddy/ │ ← TLS termination, rate limiting
│ nginx │
└──────┬──────┘
│
▼
┌─────────────┐
│ Gunicorn │ ← Multiple workers
│ (4 workers)│
└──────┬──────┘
│
▼
┌─────────────┐
│ Flask │ ← Struckdown Playground
│ App │
└─────────────┘
Environment Variables
| Variable | Description | Default |
|---|---|---|
LLM_API_KEY | API key for LLM provider (local mode only – not read in remote mode) | (required in local mode) |
LLM_API_BASE | Custom API base URL | Provider default |
DEFAULT_LLM | Default model name | gpt-4.1-mini |
STRUCKDOWN_CACHE | Cache directory (0 to disable) | ~/.struckdown/cache |
Security Settings (Remote Mode)
| Variable | Description | Default |
|---|---|---|
STRUCKDOWN_RATE_LIMIT | Rate limit for API endpoints (Flask-Limiter format) | 100/hour |
STRUCKDOWN_MAX_SYNTAX_LENGTH | Maximum template length in characters | 1000000 (1M) |
STRUCKDOWN_MAX_UPLOAD_SIZE | Maximum upload file size in bytes | 5242880 (5MB) |
STRUCKDOWN_ZIP_MAX_SIZE | Maximum uncompressed zip size in bytes | 52428800 (50MB) |
STRUCKDOWN_ZIP_MAX_FILES | Maximum files in a zip archive | 500 |
Action Restrictions: In remote mode, actions like @fetch and @search are disabled by default for security (they could be used to probe internal networks). Safe actions like @set and @timestamp remain available.
Troubleshooting
Port already in use
# Specify a different port
sd edit -p 9001
Custom actions not loading
Ensure the path exists and contains valid action files:
sd edit -I /path/to/actions myfile.sd
Browser doesn’t open
Use --no-browser and manually navigate:
sd edit --no-browser
# Then open http://localhost:9000 manually
Session data not persisting
- Check that localStorage is enabled in your browser
- Each session has a unique ID in the URL hash (
#s=sd_xxxxxxxx) - Different tabs/URLs have separate sessions
API Endpoints (for integrations)
| Endpoint | Method | Description |
|---|---|---|
/ | GET | Main editor page |
/e/<encoded> | GET | Load from URL-encoded state |
/api/analyse | POST | Validate syntax, extract inputs/slots |
/api/run | POST | Execute single prompt |
/api/save | POST | Save to file (local mode only) |
/api/upload | POST | Upload xlsx/csv for batch |
/api/run-batch | POST | Start batch task |
/api/batch-stream/<id> | GET | SSE stream of batch results |
/api/download/<id> | GET | Download batch results as xlsx |
/api/encode-state | POST | Encode state for URL sharing |
Example: Analyse endpoint
curl -X POST http://localhost:9000/api/analyse \
-H "Content-Type: application/json" \
-d '{"syntax": "Tell me about \n\n[[response]]"}'
Response:
{
"valid": true,
"error": null,
"inputs_required": ["topic"],
"slots_defined": ["response"],
"uses_history": false
}
Example: Run endpoint
curl -X POST http://localhost:9000/api/run \
-H "Content-Type: application/json" \
-d '{
"syntax": "Tell me a joke about \n\n[[joke]]",
"inputs": {"topic": "programming"},
"model": "openai:gpt-4o-mini"
}'
Response:
{
"outputs": {"joke": "Why do programmers prefer dark mode? Because light attracts bugs!"},
"cost": {"total_cost": 0.0001, "total_tokens": 45, "input_tokens": 30, "output_tokens": 15},
"error": null
}