Wexflow
Overview
Wexflow is an open-source workflow engine and automation platform, published under the MIT license. It handles the work that keeps a business running but that nobody wants to do by hand: move the files that arrived overnight, unpack and convert them, hash and encrypt them, push them over SFTP, call an API, run a query, wait for a person to approve the result, then tell somebody it is done. You assemble that from ready-made tasks in a browser instead of maintaining a pile of scripts.
A workflow is a list of tasks with settings. Wexflow ships more than one hundred tasks, covering file and folder operations, archives, encryption and hashing, HTTP calls, SQL queries, SSH and FTP transfers, image and document conversion, electronic mail and scripting. Workflows run on demand, on a cron or periodic schedule, or automatically when a file lands in a watched folder. An approval and records module lets a person sign off a step before the rest continues. Everything the interface can do is also available through a documented REST API.
This guide covers the deployment and configuration of Wexflow on Ubuntu 24.04 on Azure, using SecureAnyCloud Azure Marketplace images. The image ships the official upstream Wexflow 10.0 release, fetched from the project's own GitHub release and pinned by SHA-256. The exact artifact, its digest, its license and the runtime it was built against are recorded on the image at /opt/wexflow/VERSION. Wexflow runs under systemd as wexflow.service, under an unprivileged wexflow account, behind nginx on port 80.
The image can also prove that the engine works. A sign-in page that returns a web page tells you nothing about whether the engine can execute a task. So the image ships a small SecureAnyCloud-authored workflow, sac_Health_Check, that creates a file and copies it. Running it is a one-click proof that the engine, the scheduler, the task library and the database all work together. It is the same proof this instance ran on itself during its first boot.
What you get:
- Wexflow 10.0 (official upstream release, SHA-256 pinned, redistributed unmodified)
- The complete upstream library of example workflows, one per task type, ready to open and copy
- The
sac_Health_Checkworkflow, a one-click, end-to-end proof that the engine executes tasks - An embedded SQLite database, so there is no external database to provision or point at
- nginx on port 80 as the single entry point, with an unauthenticated
/healthzfor load balancer probes - A unique administrator password, token signing key and certificate passphrase generated on first boot
- The ASP.NET Core 10 runtime from the Ubuntu archive, so it keeps receiving security updates
- Ubuntu 24.04 LTS, fully patched at build time, with unattended security updates left enabled
- 24/7 SecureAnyCloud support
Key facts:
Item Value
Platform Ubuntu 24.04 LTS (Azure Gen2)
Default SSH user azureuser
Admin panel http://<vm-ip>/admin/
REST API base http://<vm-ip>/api/v1/
Services wexflow.service, nginx.service
Runs as wexflow (unprivileged, no login shell)
Application root /opt/wexflow
Database /opt/wexflow/Wexflow/Database/Wexflow.sqlite (embedded SQLite)
Credentials file /root/wexflow-credentials.txt (0600 root:root)
HTTP port 80
Engine port 8000 (behind the proxy; leave it closed in your network security group)
Prerequisites
Before you deploy, make sure you have:
- An active Azure subscription
- An SSH key pair, or the ability to generate one in the Azure Portal
- Permission to create virtual machines, virtual networks and network security groups
- A network security group that allows inbound TCP 22 from your management network, and inbound TCP 80 from wherever the admin panel should be reachable
Recommended VM size: Standard_B2s (2 vCPU, 4 GiB). With the full example catalogue loaded, the engine uses about 140 MiB of memory and the whole appliance about 600 MiB, so 4 GiB leaves a very large margin. Size up only for the workflows you intend to run in parallel and the files they handle.
Deploy from the Azure Portal
- In the Azure Portal, choose Create a resource and search the Marketplace for Wexflow on Ubuntu 24.04 LTS by SecureAnyCloud.
- Select the offer and choose Create.
- On the Basics tab, pick your subscription, resource group and region, set the VM name, and choose size
Standard_B2s. - Leave the authentication type as SSH public key and the username as
azureuser. - Under Inbound port rules, allow SSH (22) and HTTP (80).
- Select Review + create. The VM is ready in about a minute. First boot then generates this instance's own administrator password and token signing key, and nothing listens on port 80 until it has.
Deploy from the Azure CLI
az group create --name wexflow-rg --location eastus
az vm image terms accept --urn sac1647283583153:wexflow:default:latest
az vm create \
--resource-group wexflow-rg \
--name wexflow-vm \
--image sac1647283583153:wexflow:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
# the public address of the new VM
az vm show --resource-group wexflow-rg --name wexflow-vm \
--show-details --query publicIps --output tsvOpen the admin panel port:
az network nsg rule create --resource-group wexflow-rg \
--nsg-name wexflow-vmNSG --name allow-http \
--priority 1010 --destination-port-ranges 80 \
--protocol Tcp --access Allow --direction InboundConnect and read this instance's sign in details
ssh azureuser@<vm-ip>Every instance generates its own administrator password on first boot and writes it to a root-only file. Read it:
sudo cat /root/wexflow-credentials.txtExpected output:
# Wexflow on Ubuntu 24.04 LTS by sac — per-VM credentials
# Generated at first boot on 2026-08-07T21:31:08Z. Keep this file private.
#
# Deployment guide: https://secureanycloud.com/stacks/wexflow/ WEXFLOW_URL=http://20.124.233.51/admin/ WEXFLOW_ADMIN_USERNAME=admin WEXFLOW_ADMIN_PASSWORD=<the unique password generated for your instance>
# Sign in at http://20.124.233.51/admin/ and change this password from Users in the admin panel.
# The JWT signing key and the HTTPS pfx passphrase were also generated uniquely for this
# instance and live in /opt/wexflow/Wexflow.Server/appsettings.json (root:wexflow 0640).The address shown is your own instance's public address, and the password is unique to it. No SecureAnyCloud image ever ships a shared or published password.
Confirm that both services are up and check exactly which upstream artifact this image shipped:
systemctl is-active wexflow.service nginx.service cat /opt/wexflow/VERSION curl -s http://127.0.0.1/api/v1/helloExpected output:
active active product=Wexflow version=10.0 upstream=https://github.com/aelassas/wexflow release=https://github.com/aelassas/wexflow/releases/download/v10.0/wexflow-10.0-linux-netcore.zip release_sha256=4c7c051b12d2bea108ad4f1c819ad1849fe2daed14bdbdd1ef7e1974f406bf5a licence=MIT (verbatim text at /opt/wexflow/LICENSE.txt) runtime=aspnetcore-runtime-10.0 10.0.10-0ubuntu1~24.04.1 (Ubuntu archive, noble-updates/main) datastore=SQLite (embedded, /opt/wexflow/Wexflow/Database/Wexflow.sqlite) entry_point=http://<this-vm-public-ip>/admin/ built_by=sac {"message":"Wexflow Service is running..."}Sign in to the admin panel
Open http://<vm-ip>/admin/ in a browser. The root of the site redirects there, so http://<vm-ip>/ works too.
Sign in with the username admin and the password from /root/wexflow-credentials.txt. You land on the Dashboard, which shows live counters for pending, running, completed, failed, warning, rejected and stopped jobs, and a searchable list of recent entries.
Browse the workflow catalogue
Choose Manager in the top navigation. It lists every workflow the engine has loaded from the embedded database, with its id, launch type, whether it is enabled, whether it needs approval, and its description.
The image ships the complete upstream example catalogue, one workflow per task type, plus the SecureAnyCloud health check. Open, copy and adapt them as you like.
To count them from the shell, run:
sudo sqlite3 /opt/wexflow/Wexflow/Database/Wexflow.sqlite 'select count(*) from workflows;' ls /opt/wexflow/Wexflow/Workflows | head -6Expected output:
120 Workflow_Approval.xml Workflow_Approval_Import.xml Workflow_Approval_Reject.xml Workflow_ApproveDocument.xml Workflow_ApproveInvoice.xml Workflow_ApproveTimesheet.xmlThree of the upstream examples launch themselves: one at every startup, one periodically and one on a cron schedule. They ship disabled, so a new instance does not silently execute jobs and accumulate history before you have looked at it. Step 9 shows how to switch one on.
Run a workflow and watch it finish
In Manager, click the row for sac_Health_Check to select it, then click Start.
Go back to Dashboard. The Done counter has advanced, and the run appears in the entries table with a green Done status, its id, its name and the time it finished.
The workflow did real work: it created a file and copied it. Confirm on the instance:
sudo ls -l /var/lib/wexflow/healthcheck/out/
sudo journalctl -u wexflow.service --no-pager | grep sac_Health_Check | tail -6Expected output:
total 0 -rw-r--r-- 1 wexflow wexflow 0 Aug 7 21:31 sac-healthcheck.txt 2026-08-07 21:29:45,309 INFO [13] - [sac_Health_Check / 6] [Touch] File /var/lib/wexflow/healthcheck/src/sac-healthcheck.txt created. 2026-08-07 21:29:45,309 INFO [13] - [sac_Health_Check / 6] [Touch] Task finished. 2026-08-07 21:29:45,309 INFO [13] - [sac_Health_Check / 6] [FilesCopier] Copying files... 2026-08-07 21:29:45,309 INFO [13] - [sac_Health_Check / 6] [FilesCopier] File copied: /var/lib/wexflow/healthcheck/src/sac-healthcheck.txt -> /var/lib/wexflow/healthcheck/out/sac-healthcheck.txt 2026-08-07 21:29:45,309 INFO [13] - [sac_Health_Check / 6] [FilesCopier] Task finished. 2026-08-07 21:29:45,310 INFO [13] - [sac_Health_Check / 6] Workflow finished.The same proof runs as a single command, which is what this instance ran on itself during first boot. The command also re-proves that every published upstream default credential is refused:
sudo /usr/local/sbin/wexflow-verify-workflow.shExpected output:
wexflow runtime verification ok all five published upstream default credentials rejected with 401 ok the second seeded administrator account no longer exists ok a token forged with the published upstream signing key is rejected ok privileged endpoints reject unauthenticated calls ok the per-VM admin password authenticates and returns a token ok the token opens a privileged endpoint ok workflow 900 started, instance 237462e8-6e02-4b44-92f6-28ee42d621a2 ok the workflow produced /var/lib/wexflow/healthcheck/out/sac-healthcheck.txt ok the engine's completed-workflow counter advanced 5 -> 6 WEXFLOW_VERIFY_OKBuild your own workflow in the designer
Choose Designer in the top navigation, click Browse, pick a workflow and click Open. The workflow renders as a diagram of its tasks in order. You can also switch the same workflow to the Graph, JSON or XML view, edit it there, and save.
Choose New workflow to start an empty one. Give it an unused id, a name and a description, then choose a launch type. Add tasks from the palette and set their options. Save writes the workflow to the database, and the engine picks it up immediately. Run starts it without leaving the designer.
The example catalogue is the fastest way to learn a task. Open the example named after the task, read its settings, then copy the pattern into your own workflow. Every task is also documented in XML on the instance:
ls /opt/wexflow/Documentation | head -8 cat /opt/wexflow/Documentation/FilesCopier.xmlReview history and approvals
History lists every run the engine has ever recorded, with its status, date, workflow and description. Filter it by date range and free text. Dashboard shows the same information for the current period, along with the live counters.
Approvals and Records are the human-in-the-loop side of the product. A workflow marked as an approval workflow pauses at its Approval task until somebody approves or rejects it from the Approvals page. Records attach documents to that decision, and the records hot folder at /opt/wexflow/Wexflow/Records/_HotFolder picks up files dropped into it.
Schedule a workflow
A workflow's launch type decides when it runs. Set it in the designer, in the workflow XML, or through the API.
Launch type When it runs
trigger Only when you start it, from the manager, the designer or the API
startup Once, every time the engine starts
periodic Every period, expressed as dd.hh:mm:ss
cron On a Quartz cron expression, for example 0 0/1 * * * ? for every minute
The three shipped examples that use the last three launch types are disabled. To look at one before enabling it:
sudo sqlite3 /opt/wexflow/Wexflow/Database/Wexflow.sqlite \
"select XML from workflows where XML like '%Workflow_Cron%' limit 1;" | head -12To enable it, open it in the designer, select Enabled and save. Do this rather than editing the database, so the engine's in-memory schedule stays in step with what is stored.
File-triggered workflows are the other common pattern. A workflow whose first task is FileSystemWatcher starts itself when a file appears in the folder it watches. Most overnight file handling is wired up this way. cat /opt/wexflow/Documentation/FileSystemWatcher.xml documents its options.
Use the REST API
Everything the interface does is available over HTTP. Sign in once to get a token, then send it as a bearer token. In the commands below, <WEXFLOW_ADMIN_PASSWORD> stands for the password from /root/wexflow-credentials.txt.
TOKEN=$(curl -s -X POST http://127.0.0.1/api/v1/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"<WEXFLOW_ADMIN_PASSWORD>","stayConnected":false}' \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1/api/v1/status-count echo
curl -s -H "Authorization: Bearer $TOKEN" 'http://127.0.0.1/api/v1/search?s=sac' \
| python3 -m json.tool | head -12Expected output:
{"PendingCount":0,"RunningCount":0,"DoneCount":2,"FailedCount":0,"WarningCount":0,"DisabledCount":3,"RejectedCount":0,"StoppedCount":0} [ { "DbId": "120", "Id": 900, "InstanceId": "3a344963-066d-4280-971b-bc9d4a4752f8", "Name": "sac_Health_Check", "FilePath": null, "LaunchType": 1, "IsEnabled": true, "IsApproval": false, "EnableParallelJobs": true,Start a workflow and read back the engine's counters:
TOKEN=$(curl -s -X POST http://127.0.0.1/api/v1/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"<WEXFLOW_ADMIN_PASSWORD>","stayConnected":false}' \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')
curl -s -X POST -H "Authorization: Bearer $TOKEN" 'http://127.0.0.1/api/v1/start?w=900' echo sleep 5
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1/api/v1/status-count echoA browsable description of the whole API is served at http://<vm-ip>/swagger. To call the API from your own machine, replace 127.0.0.1 with your instance's address.
Change the administrator password and add users
Treat the generated password as a starting point. Change it from Users in the admin panel: select admin, set a new password and save. The same page adds further users.
Wexflow has three user profiles. A super administrator sees and controls every workflow. An administrator sees only the workflows assigned to them, which you set per user on the Users page. A restricted user has no manager access at all and is used for the approvals and records side of the product.
The engine looks up the account named admin at startup, so keep that name. Change its password freely.
Serve the admin panel over TLS
The image ships on plain HTTP so that it works the moment it boots, with no certificate and no domain name required. For anything beyond a trial, put a certificate in front of it. nginx is already the entry point, so this is ordinary nginx work:
- Point a DNS name at the instance and open TCP 443 in the network security group.
- Install a certificate for that name, either your own or one from a certificate authority.
- Add a
listen 443 ssl;server block to/etc/nginx/sites-available/wexflowwith your certificate paths. Keep the samelocationblocks, and redirect port 80 to it. - Reload with
sudo nginx -t && sudo systemctl reload nginx.
The admin single-page application addresses the API on the same origin it was loaded from, so it follows the change with no further configuration.
Server components
Component Version Purpose
Wexflow 10.0 The workflow engine, admin panel and REST API
ASP.NET Core runtime 10.0 The runtime the engine is built against, from the Ubuntu archive
nginx 1.24 Reverse proxy and the single public entry point on port 80
SQLite 3.45 The embedded database holding workflows, users, entries and records
Ubuntu Server 24.04 LTS Operating system, fully patched at build time
Filesystem layout
Path Size Purpose
/ 29 GB Root filesystem
/boot 881 MB Operating system kernel files
/boot/efi 100 MB UEFI boot partition (Gen2 Hyper V)
/mnt varies Azure temporary resource disk
Key directories:
Path Purpose
/opt/wexflow Application root
/opt/wexflow/Wexflow.Server The engine, its appsettings.json and the admin panel host
/opt/wexflow/Admin The admin single page application
/opt/wexflow/Wexflow/Database The embedded SQLite database
/opt/wexflow/Wexflow/Workflows Workflow definitions on disk
/opt/wexflow/Wexflow/Records Records, including the _HotFolder watched folder
/opt/wexflow/Wexflow/Temp Scratch space used during a run
/opt/wexflow/Documentation One XML reference page per task type
/opt/wexflow/WexflowTesting Sample input files the example workflows use
/var/lib/wexflow Working directory of the service account
/var/log/wexflow Engine log
Managing the services
sudo systemctl status wexflow.service --no-pager | head -6 sudo systemctl restart wexflow.service
sudo systemctl reload nginxExpected output:
● wexflow.service - Wexflow workflow engine Loaded: loaded (/etc/systemd/system/wexflow.service; enabled; preset: enabled) Active: active (running) since Fri 2026-08-07 21:31:08 UTC; 30s ago Docs: https://secureanycloud.com/stacks/wexflow/ Main PID: 9201 (dotnet) Tasks: 22 (limit: 4666)Both units are enabled, so the appliance comes back on its own after a reboot.
Scripts and log files
Path Purpose
/usr/local/sbin/wexflow-firstboot.sh First boot bootstrap: mints this instance's secrets, then releases the services
/usr/local/sbin/wexflow-verify-workflow.sh The runtime proof used in Step 6, safe to re-run at any time
/usr/local/lib/sac/wexflow_pwhash.py The password hashing helper first boot uses
/var/log/sac/sac-firstboot.log What first boot did on this instance
/var/log/wexflow/Wexflow.log Engine log, one file per day
/var/log/nginx/access.log Proxy access log
/etc/nginx/sites-available/wexflow The reverse proxy configuration
sudo tail -n 20 /var/log/sac/sac-firstboot.log
sudo tail -n 20 /var/log/wexflow/Wexflow.logOn startup
The first time an instance boots, wexflow-firstboot.service runs once. It generates a unique administrator password, a unique token signing key and a unique certificate passphrase. It writes the password into /root/wexflow-credentials.txt with mode 0600, and only then creates the marker that releases the engine and nginx.
Both wexflow.service and nginx.service carry ConditionPathExists on that marker, so until first boot has finished, nothing is listening on port 80 at all. A newly launched instance is never reachable with a credential that came from the image.
First boot then verifies the result before it hands the instance over. Every published upstream default credential is refused, a token forged with the published upstream signing key is refused, the new password works, and a real workflow runs to completion. If any check fails, the service is left down rather than handed over broken.
sudo systemctl is-active wexflow-firstboot.service
sudo test -f /var/lib/sac/data/wexflow-firstboot.done && echo "first boot complete"Expected output:
active first boot completeTroubleshooting
The admin panel does not load. Check that first boot finished and that both services are up:
sudo systemctl is-active wexflow-firstboot.service wexflow.service nginx.service
sudo test -f /var/lib/sac/data/wexflow-bootstrap-ready && echo "bootstrap ready marker present" curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/healthzAll three units report active, the marker is present and /healthz returns 200. If the units are inactive and the marker is missing, first boot did not finish. Read /var/log/sac/sac-firstboot.log and sudo journalctl -u wexflow-firstboot.service.
The panel loads on the instance but not from my browser. The network security group is blocking it. Port 80 must be open inbound from wherever you are browsing. Port 8000 is the engine behind the proxy and should stay closed.
The password in the credentials file is rejected. The file is written only on the very first boot. If you changed the password afterwards from the Users page, the file still shows the original one. Change it back from an account that can still sign in, or see the next item.
Nobody can sign in at all. Reset the account from the instance. This writes a new hash directly into the embedded database, using the same scheme the engine uses. Replace <new-password> with the password you want. The password is read on standard input rather than passed as an argument, so it never reaches the process list or the journal:
sudo systemctl stop wexflow.service NEWHASH="$(printf '%s' '<new-password>' | sudo /usr/local/lib/sac/wexflow_pwhash.py hash)"
sudo sqlite3 /opt/wexflow/Wexflow/Database/Wexflow.sqlite \
"update users set PASSWORD='${NEWHASH}' where USERNAME='admin';"
sudo systemctl start wexflow.serviceSign in with the new password, then update /root/wexflow-credentials.txt or your own password store so the two agree again.
A workflow finished with a warning or failed. Open it from History, or read the engine log. Every task logs what it did and why it stopped:
sudo tail -n 40 /var/log/wexflow/Wexflow.logA workflow cannot write where I told it to. The engine runs as the unprivileged wexflow account, not as root. It can write inside /opt/wexflow/Wexflow, /opt/wexflow/WexflowTesting and /var/lib/wexflow. For any other location, create the directory and give it to the service account:
sudo install -d -o wexflow -g wexflow -m 0750 /srv/my-workflow-dataSigned-out pages work but signed-in pages return 502. A proxy buffer is too small for the signed-in session cookie. The shipped configuration already raises it. If you have replaced /etc/nginx/sites-available/wexflow, keep the proxy_buffer_size 32k; and proxy_buffers 8 32k; lines.
Check the appliance as a whole. The single command from Step 6 re-proves the sign-in path, the credential hygiene and a real workflow run in one pass:
sudo /usr/local/sbin/wexflow-verify-workflow.shSecurity notes
- No shared or published credential ships in the image. Upstream seeds two
administrator accounts with the same published password. The image removes the second account entirely. At build time, it replaces the stored hash of the remaining admin account with a value nobody records, so the image contains no hash of any published password. - The token signing key is per instance. Wexflow signs its session tokens with an HMAC key from its settings file. The published upstream value would let anyone mint a valid token for any user without touching the sign-in page. The image therefore replaces it with a non-functional value, and every first boot generates a fresh key. The verification in Step 6 proves that a token signed with the published key is refused.
- Nothing listens until the instance has its own secrets. Both services are gated on a marker that first boot creates only after every per-instance secret is written and verified.
- The engine runs unprivileged. It runs as the
wexflowsystem account with no login shell andNoNewPrivileges, not as root. - Change the generated password from the Users page, and add a separate account per person rather than sharing
admin. - Restrict the network security group. Allow SSH only from your management network, and port 80 only from where the admin panel needs to be reachable. Leave port 8000 closed.
- Put TLS in front of the admin panel before you use it over anything other than a trusted network, as in Step 12. Session tokens travel in a cookie, and on plain HTTP that cookie is readable on the wire.
- Wexflow is an automation engine, and that is the point. Its task library deliberately launches processes, runs scripts and moves files. Treat access to the designer as equivalent to shell access on the instance, and give it only to people you would trust with shell access.
- Keep the operating system patched. Unattended security updates are enabled. Reboot when a kernel update lands.
Support
- Deployment guides: secureanycloud.com/guides
- Product catalog: secureanycloud.com/products
- 24/7 support: sales@secureanycloud.com
Wexflow is a trademark of its respective owners. SecureAnyCloud is not affiliated with, endorsed by or sponsored by the Wexflow project. The name is used nominatively to identify the open-source software packaged in this image. Wexflow is distributed under the MIT license, and the verbatim license text is retained on the image at /opt/wexflow/LICENSE.txt.
Deploy on Azure
Launch Wexflow on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com