Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 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
df6a93b
fix(deploy): check the retired key in the deploy script, not in compose
dawid-aksamski Sep 15, 2026
170ae70
fix(backend): validate TEMPORAL_* at boot, like the worker
dawid-aksamski Sep 15, 2026
ce77282
feat(ai-config): name a retired AI variable that is still set
dawid-aksamski Sep 15, 2026
8f24384
test(backend): pin the env defaults the deploy depends on
dawid-aksamski Sep 15, 2026
a9ba20b
test(temporal-connection): prove the plaintext default connects
dawid-aksamski Sep 15, 2026
30b3510
fix(ai-studio): drop the provider name from the disclaimer
dawid-aksamski Sep 15, 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
47 changes: 40 additions & 7 deletions .github/workflows/deploy-ai-studio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,25 +77,58 @@ 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 retired-key check runs before anything is written, so a refused deploy
# leaves the VM exactly as it was. It lives here rather than in the compose
# file because Compose 2.21 and older evaluate a nested `${A:+${B:?}}` guard
# eagerly and fail on every command, key set or not.
#
# 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
if [ -f .env ] && grep -Eq '^OPENROUTER_API_KEY=.+' .env; then
echo "OPENROUTER_API_KEY is still set in the VM's .env. It was renamed to AI_API_KEY and is no longer read; rename it and set AI_BASE_URL and AI_MODEL too (values in deploy/ai-studio/.env.example), then deploy again."
exit 1
fi
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 && chown --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
47 changes: 38 additions & 9 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ name: PR Check
# @workflowbuilder/ui-tokens build) and @workflowbuilder/temporal, and the
# execution pipeline (execution-core, backend, execution-worker) — whose
# determinism tests guard Temporal replay safety and so must not be able to
# regress silently. Plus
# global format consistency. apps/docs has its own path-filtered workflow
# regress silently. Plus the deploy compose files, which ship to the demo VM on
# every deploy, and global format consistency. apps/docs has its own path-filtered workflow
# (pr-check-docs.yml); demo and ai-studio are not checked here — they're
# internal and have their own broken-state tolerances.

Expand Down Expand Up @@ -170,6 +170,34 @@ jobs:
fi
fi

deploy-compose:
name: Deploy compose files parse
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Parse both compose modes on the runner's Compose and on the oldest supported one
# These files reach the demo VM on every deploy, so a parse error is only
# discovered there, with the stack already down. 2.21 is the floor: it
# interpolates a nested `${A:+${B:?}}` default eagerly where newer
# compose-go is lazy, so a file that parses on the runner can still fail
# on a VM. Both modes are covered because COMPOSE_FILE in the VM's .env
# decides whether the override file is applied at all.
working-directory: deploy/ai-studio
run: |
cp .env.example .env
# An empty COMPOSE_FILE is not the same as an unset one — compose then
# reads the working directory as a file — so the default mode runs with
# the variable absent and `-e` forwards it only once it is exported.
parse() {
docker compose config --quiet
docker run --rm -v "$PWD:/w" -w /w -e COMPOSE_FILE docker:24.0.5-cli docker compose config --quiet
}
parse
export COMPOSE_FILE=docker-compose.yml
parse

ui:
name: UI + UI tokens lint + typecheck + test + build
runs-on: ubuntu-latest
Expand Down Expand Up @@ -219,10 +247,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 +273,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
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function DisclaimerModal() {
workflow editors.
</p>
<p>
The workflows here run for real: every AI step calls a live model through <strong>OpenRouter</strong>.
The workflows here run for real: every AI step calls a <strong>live model</strong>.
</p>
<p>
It is <strong>not</strong> a place to test or benchmark AI models. The model is just the engine — the point
Expand Down
Loading
Loading