Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eb7f34b
feat(backend): call any OpenAI-compatible endpoint from the AI adapt …
dawid-aksamski Sep 1, 2026
7a50877
feat(execution-worker): boot without an LLM key; AI nodes fail at cal…
dawid-aksamski Sep 1, 2026
f11e3f4
feat(backend): Temporal namespace and TLS/mTLS/API-key connection config
dawid-aksamski Sep 1, 2026
552a823
feat(execution-worker): Temporal namespace and TLS/mTLS/API-key conne…
dawid-aksamski Sep 1, 2026
d824d39
build(deps): put the AI SDK packages in the catalog
dawid-aksamski Sep 1, 2026
dc01fa5
feat(deploy): pass LLM endpoint and Temporal connection config throug…
dawid-aksamski Sep 1, 2026
9b468ed
fix(config): drop the OpenRouter key alias and built-in LLM defaults
dawid-aksamski Sep 4, 2026
be9bf5b
fix(execution-worker): make ai_not_configured a permanent failure
dawid-aksamski Sep 4, 2026
9a336bc
feat(deploy): pass Temporal TLS paths through compose and mount ./tls
dawid-aksamski Sep 4, 2026
e947785
feat(deploy): let an external Temporal retire the bundled cluster
dawid-aksamski Sep 4, 2026
1d5c39b
test(config): isolate env tests from the runner's environment
dawid-aksamski Sep 4, 2026
168c5d9
docs(site): document secured and external Temporal configuration
dawid-aksamski Sep 4, 2026
0173914
fix(deploy): run compose from the project dir and ship both files to …
dawid-aksamski Sep 4, 2026
b88c929
fix(deploy): keep certificate files out of the image build context
dawid-aksamski Sep 4, 2026
e0123d9
fix(config): ship .env.example with an empty AI_API_KEY
dawid-aksamski Sep 4, 2026
9337b8b
docs: align the root README with the no-default LLM configuration
dawid-aksamski Sep 4, 2026
f86f26f
fix(deploy): refuse to start while OPENROUTER_API_KEY is still set
dawid-aksamski Sep 4, 2026
f3ef683
docs: correct the Temporal failure mode and remove default-wording drift
dawid-aksamski Sep 4, 2026
984097f
fix(deploy): keep custom TEMPORAL_TLS_DIR out of the image build context
dawid-aksamski Sep 8, 2026
e434d87
fix(ci): persist deployed image tags in the VM's .env
dawid-aksamski Sep 8, 2026
db829b7
test(backend,execution-worker): prove TLS, mTLS and API-key transport…
dawid-aksamski Sep 8, 2026
e04466b
refactor(temporal-connection): one copy of the TEMPORAL_* rules
dawid-aksamski Sep 8, 2026
0b21add
refactor(ai-config): share the AI_* contract, keep the runtime reacti…
dawid-aksamski Sep 8, 2026
e9ccc5b
docs: limit the "no external traffic" promise to model requests
dawid-aksamski Sep 8, 2026
d6bc032
refactor: drop a header that restated the function, name the PEM reader
dawid-aksamski Sep 8, 2026
1dbca05
test(temporal-connection): remove the TLS test's temp PKI directories
dawid-aksamski Sep 8, 2026
108e998
docs(deploy): the backend calls the LLM too, for the visualize route
dawid-aksamski Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,15 @@ examples/
**/.env
**/.env.*
!**/.env.example

# certificate material is mounted at runtime — never built into an image. The
# Dockerfile needs one file from deploy/, so the rest stays out of the context:
# a TEMPORAL_TLS_DIR under deploy/ cannot reach COPY . . whatever it is named.
deploy/
!deploy/ai-studio/nginx
**/*.pem
**/*.key
**/*.crt
**/*.cer
**/*.p12
**/*.pfx
38 changes: 31 additions & 7 deletions .github/workflows/deploy-ai-studio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,25 +77,49 @@ jobs:
needs: build-and-push

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Log in to Azure
uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

# The VM runs the repo's compose files, shipped here on every deploy (base64,
# so the script stays free of quoting). Compose is run from the project
# directory, not with -f: that is what applies docker-compose.override.yml
# by default and honours COMPOSE_FILE from the VM's .env.
#
# The image tags are written into that .env rather than exported: an export
# dies with this shell, and the next `docker compose up -d worker` on the VM
# would fall back to the local ai-studio-* names. Only the two image lines
# are replaced; the rest of .env is the VM's own and stays untouched.
- name: Refresh docker compose on Azure VM
env:
IMAGE: ${{ env.REGISTRY }}/${{ env.APP }}:${{ needs.build-and-push.outputs.image_tag }}
run: |
COMPOSE_B64=$(base64 -w0 deploy/ai-studio/docker-compose.yml)
OVERRIDE_B64=$(base64 -w0 deploy/ai-studio/docker-compose.override.yml)
SCRIPT=$(cat <<EOF
set -e
cd /app/ai-studio
echo "$COMPOSE_B64" | base64 -d > docker-compose.yml
echo "$OVERRIDE_B64" | base64 -d > docker-compose.override.yml
touch .env
{ grep -vE '^(RUNTIME_IMAGE|WEB_IMAGE)=' .env || true; printf 'RUNTIME_IMAGE=%s\nWEB_IMAGE=%s\n' "$IMAGE-runtime" "$IMAGE-web"; } > .env.tmp
chmod --reference=.env .env.tmp && mv .env.tmp .env
az acr login --name synergycodes
docker compose pull
docker compose up -d --no-build --force-recreate --remove-orphans
echo DEPLOY_SCRIPT_SUCCEEDED
EOF
)
OUTPUT=$(az vm run-command invoke \
--name ${{ vars.AI_STUDIO_VM_NAME }} \
--resource-group ${{ vars.AI_STUDIO_VM_RG }} \
--command-id RunShellScript \
--scripts '
set -e
az acr login --name synergycodes
docker compose -f /app/ai-studio/docker-compose.yml pull
docker compose -f /app/ai-studio/docker-compose.yml up -d --no-build --force-recreate
echo DEPLOY_SCRIPT_SUCCEEDED
')
--scripts "$SCRIPT")
echo "$OUTPUT"
echo "$OUTPUT" | grep -q DEPLOY_SCRIPT_SUCCEEDED
15 changes: 8 additions & 7 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,11 @@ jobs:
execution:
name: Execution pipeline lint + typecheck + test
runs-on: ubuntu-latest
# No `services:` block: all three suites are pure unit tests against
# in-memory fakes — no Postgres, no Temporal, no API keys. If a suite here
# ever needs real infra, give it its own job rather than adding services
# to this one.
# No `services:` block: the suites run against in-memory fakes — no Postgres,
# no API keys. The one exception is temporal-connection's TLS test, which
# starts Temporal's dev server itself (@temporalio/testing downloads the CLI
# on first run). If a suite here ever needs infra it cannot start itself,
# give it its own job rather than adding services to this one.
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -244,10 +245,10 @@ jobs:
run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm --filter @workflow-builder/execution-core --filter @workflow-builder/backend --filter @workflow-builder/execution-worker lint
run: pnpm --filter @workflow-builder/execution-core --filter @workflow-builder/ai-config --filter @workflow-builder/temporal-connection --filter @workflow-builder/backend --filter @workflow-builder/execution-worker lint

- name: Typecheck
run: pnpm --filter @workflow-builder/execution-core --filter @workflow-builder/backend --filter @workflow-builder/execution-worker typecheck
run: pnpm --filter @workflow-builder/execution-core --filter @workflow-builder/ai-config --filter @workflow-builder/temporal-connection --filter @workflow-builder/backend --filter @workflow-builder/execution-worker typecheck

- name: Test
run: pnpm --filter @workflow-builder/execution-core --filter @workflow-builder/backend --filter @workflow-builder/execution-worker test
run: pnpm --filter @workflow-builder/execution-core --filter @workflow-builder/ai-config --filter @workflow-builder/temporal-connection --filter @workflow-builder/backend --filter @workflow-builder/execution-worker test
30 changes: 18 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ apps/
icons/ - Icon generation pipeline
tools/ - @workflow-builder/tools workspace (decision-log collector, lint-staged config)
packages/
ai-config/ - Private, source-only: the AI_API_KEY / AI_BASE_URL / AI_MODEL contract, one copy shared by backend and worker
sdk/ - @workflowbuilder/sdk public package (WorkflowBuilder compound component, plugin API, components)
ui/ - @workflowbuilder/ui published component library (Base UI), consumed by sdk/demo/ai-studio
tokens/ - @workflowbuilder/ui-tokens private design-token build (style-dictionary), feeds packages/ui
execution-core/ - Pure topological graph runner + node executor registry
temporal/ - @workflowbuilder/temporal published Temporal Plugin (activities + workflow runner); bundles execution-core + types into its dist
temporal-connection/ - Private, source-only: TEMPORAL_* env -> validated connection options + namespace, one copy shared by backend and worker
types/ - Shared TypeScript types
```

Expand All @@ -74,21 +76,25 @@ Where to put a new script: root `tools/` for pure-Node bootstrap (runs before an

Each workspace has its own context. Read the relevant file before extending a workspace.

| Workspace | Authoritative docs |
| ------------------------- | ------------------------------------------------------- |
| `packages/sdk` | `packages/sdk/README.md` |
| `packages/ui` | `packages/ui/README.md` (+ `packages/ui/css-layers.md`) |
| `packages/tokens` | `packages/tokens/README.md` |
| `packages/execution-core` | `packages/execution-core/README.md` |
| `packages/temporal` | `packages/temporal/README.md` |
| `apps/demo` | `apps/demo/CLAUDE.md` |
| `apps/ai-studio` | `apps/ai-studio/README.md` |
| `apps/backend` | `apps/backend/README.md` |
| `apps/execution-worker` | `apps/execution-worker/README.md` |
| Workspace | Authoritative docs |
| ------------------------------ | ------------------------------------------------------- |
| `packages/sdk` | `packages/sdk/README.md` |
| `packages/ui` | `packages/ui/README.md` (+ `packages/ui/css-layers.md`) |
| `packages/tokens` | `packages/tokens/README.md` |
| `packages/ai-config` | `packages/ai-config/README.md` |
| `packages/execution-core` | `packages/execution-core/README.md` |
| `packages/temporal` | `packages/temporal/README.md` |
| `packages/temporal-connection` | `packages/temporal-connection/README.md` |
| `apps/demo` | `apps/demo/CLAUDE.md` |
| `apps/ai-studio` | `apps/ai-studio/README.md` |
| `apps/backend` | `apps/backend/README.md` |
| `apps/execution-worker` | `apps/execution-worker/README.md` |

## Types & Aliases

Shared types: `packages/types/` (imported as `@workflow-builder/types/*`).
AI configuration contract: `packages/ai-config/` (imported as `@workflow-builder/ai-config`; `aiConfig()` tells backend and worker whether the LLM is configured and what is missing).
Temporal connection config: `packages/temporal-connection/` (imported as `@workflow-builder/temporal-connection`; `temporalConfig()` gives backend and worker their connect options and namespace).
Icons: `apps/icons/` (imported as `@workflow-builder/icons`).
SDK: `packages/sdk/` (imported as `@workflowbuilder/sdk`).
UI: `packages/ui/` (imported as `@workflowbuilder/ui`; styles via `@workflowbuilder/ui/styles.css`, `/index.css`, `/tokens.css`).
Expand All @@ -102,7 +108,7 @@ UI: `packages/ui/` (imported as `@workflowbuilder/ui`; styles via `@workflowbuil
- Temporal server on `7233` (gRPC)
- Temporal UI on http://localhost:8233

Backend reads `DATABASE_URL` and `TEMPORAL_ADDRESS`; defaults work out of the box. `pnpm infra:down` stops everything.
Backend reads `DATABASE_URL` and `TEMPORAL_ADDRESS`; defaults work out of the box. Pointing either app at a secured cluster or Temporal Cloud is env-only (`TEMPORAL_NAMESPACE`, `TEMPORAL_TLS`, `TEMPORAL_API_KEY`, `TEMPORAL_TLS_*_PATH`) - see `apps/backend/README.md` "Connecting to a secured Temporal cluster". `pnpm infra:down` stops everything.

## Code Quality

Expand Down
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,30 +197,31 @@ Temporal ready
[ai-studio] ➜ Local: http://127.0.0.1:4201/
```

Open `http://localhost:4201`. Pick the "Sales Inquiry" template, click Play. The Temporal UI at `http://localhost:8233` shows the running execution.
Open `http://localhost:4201`. Every bundled template contains AI Agent nodes, so either connect an LLM first (next section) or expect the run to stop at its first AI Agent node with `ai_not_configured` while the Trigger, Decision and Visualize nodes before it run. Pick a template, click Play. The Temporal UI at `http://localhost:8233` shows the running execution.

To stop: `Ctrl+C`, then `pnpm infra:down`.

#### Connect a real LLM (optional)

AI Studio works with stub responses out of the box. To use a real model, add to both `apps/backend/.env` and `apps/execution-worker/.env`:
The stack starts without an LLM: Trigger, Decision and Visualize nodes run as usual, and an AI Agent node fails with `ai_not_configured` when the run reaches it. AI nodes need three variables in both `apps/backend/.env` and `apps/execution-worker/.env`. The files `pnpm setup:env` created already carry an endpoint and a model for [OpenRouter](https://openrouter.ai), so only the key is missing:

```env
OPENROUTER_API_KEY=sk-or-v1-...
AI_MODEL=anthropic/claude-3.5-haiku
AI_API_KEY=sk-or-v1-...
AI_BASE_URL=https://openrouter.ai/api/v1
AI_MODEL=mistralai/mistral-small-3.2-24b-instruct
```

If the key is missing the worker fails to start with `OPENROUTER_API_KEY is required`. If the model id is wrong the first AI node fails at runtime and the error surfaces in the UI log panel.
None of the three has a built-in default. Any OpenAI-compatible endpoint works: set `AI_BASE_URL` to a gateway or to a model hosted inside your own network, `AI_MODEL` to an id that endpoint understands, and model requests stay inside it. That covers the model only: the optional web-search tool calls Tavily's API when `TAVILY_API_KEY` is set, so leave it unset if nothing may call out. If the model id is wrong, the first AI node fails at runtime and the error surfaces in the UI log panel.

### Troubleshooting

| Symptom | Cause | Fix |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `EADDRINUSE` on 3001, 4200, 4201, 5432, 5433, 7233, or 8233 | Another process holds the port | `pnpm preflight` shows the conflict. Stop the other process or change the port |
| Temporal UI loads but the `default` namespace is missing | Migrations not run | `pnpm -F backend db:migrate` |
| Worker exits with `OPENROUTER_API_KEY is required` | Real LLM env var missing | Set it in `apps/execution-worker/.env`. Optional unless you want a real LLM call |
| `pnpm dev:demo` shows TypeScript errors but the dev server still starts | `concurrently` runs typecheck alongside Vite. TS errors are non-fatal | Fix the errors or ignore them temporarily |
| Vite acts up after a dependency change | Stale `node_modules/.vite` | `rm -rf node_modules/.vite` and rerun |
| Symptom | Cause | Fix |
| ----------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `EADDRINUSE` on 3001, 4200, 4201, 5432, 5433, 7233, or 8233 | Another process holds the port | `pnpm preflight` shows the conflict. Stop the other process or change the port |
| Temporal UI loads but the `default` namespace is missing | Migrations not run | `pnpm -F backend db:migrate` |
| AI Agent node fails with `ai_not_configured` | LLM not configured — the worker starts anyway, only AI nodes are unavailable | Set `AI_API_KEY`, `AI_BASE_URL` and `AI_MODEL` in `apps/execution-worker/.env` |
| `pnpm dev:demo` shows TypeScript errors but the dev server still starts | `concurrently` runs typecheck alongside Vite. TS errors are non-fatal | Fix the errors or ignore them temporarily |
| Vite acts up after a dependency change | Stale `node_modules/.vite` | `rm -rf node_modules/.vite` and rerun |

For the full command reference, see the table in [`CLAUDE.md`](./CLAUDE.md) or the documentation site.

Expand Down
35 changes: 31 additions & 4 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
DATABASE_URL=postgresql://wb:wb@127.0.0.1:5432/workflow_builder
TEMPORAL_ADDRESS=127.0.0.1:7233
# Must match the worker's namespace. Leave as `default` for the bundled dev cluster.
TEMPORAL_NAMESPACE=default
# Connection security. All optional, and all default to a plaintext connection —
# which is what the bundled dev cluster expects.
#
# TEMPORAL_TLS: leave empty to infer (setting any credential below turns TLS on),
# `true` to require TLS with the OS trust store, `false` to assert plaintext.
TEMPORAL_TLS=
# API key auth, as used by Temporal Cloud. Implies TLS.
TEMPORAL_API_KEY=
# Paths to PEM files, read when the connection opens. CA for a private issuer;
# the cert/key pair for mTLS (set both or neither, and not alongside an API key).
TEMPORAL_TLS_CA_PATH=
TEMPORAL_TLS_CERT_PATH=
TEMPORAL_TLS_KEY_PATH=
#
# Temporal Cloud looks like this:
# TEMPORAL_ADDRESS=<namespace>.<accountId>.tmprl.cloud:7233
# TEMPORAL_NAMESPACE=<namespace>.<accountId>
# TEMPORAL_API_KEY=<key>

PORT=3001
# Hostname to bind. Default 127.0.0.1 (loopback only - single-tenant local dev).
# Change ONLY if you understand: this server has no auth, anyone reachable on
Expand All @@ -15,8 +36,14 @@ WB_AUTH_PORT=allow-all
# verification (local dev). When set, POST /api/workflows/:id/execute requires a
# valid Turnstile token sent by the frontend as the cf-turnstile-token header.
TURNSTILE_SECRET_KEY=
# OpenRouter key for the Visualize "AI adapt" endpoint (POST /api/visualize/adapt).
# Optional: leave empty to disable AI adapt (the endpoint returns 501). The
# execution worker keeps its own key for running workflows.
OPENROUTER_API_KEY=
# API key for the Visualize "AI adapt" endpoint (POST /api/visualize/adapt).
# Optional: leave empty to disable AI adapt (the endpoint returns 501). The three
# AI_* variables are all-or-nothing (see packages/ai-config/README.md). The
# execution worker reads its own copy of them. OpenRouter keys look like sk-or-v1-...
AI_API_KEY=
# Any OpenAI-compatible endpoint — a hosted gateway or a model inside your own
# network. Must be the base URL, without a trailing /chat/completions.
# Pre-filled with OpenRouter's URL; there is no built-in default.
AI_BASE_URL=https://openrouter.ai/api/v1
# Model id as the endpoint above understands it.
AI_MODEL=mistralai/mistral-small-3.2-24b-instruct
Loading
Loading