Ready-made setups for common jobs. Each uses the CLI in GitHub Actions; the commands are the same in any CI, see the CI guides.
End-to-End on every push, Security nightly
A quick End-to-End scan on every push to main, and the fuller Security scan once a night, when nobody's waiting on it:
name: ValiqAI
on:
push:
branches: [main]
schedule:
- cron: '0 3 * * *' # 03:00 UTC
jobs:
valiqai:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: 22
- name: ValiqAI scan
run: |
if [ "${{ github.event_name }}" = "schedule" ]; then
npx -y @valiqai/cli scan "$STAGING_URL" --type security --chatbot-consent --fail-on high --label nightly
else
npx -y @valiqai/cli scan "$STAGING_URL" --type e2e --fail-on blocker
fi
env:
VALIQAI_API_KEY: ${{ secrets.VALIQAI_API_KEY }}
STAGING_URL: https://staging.example.comAdd --chatbot-consent only if you're allowed to security-test the site's chatbot. What each scan costs is in Choose a scan.
Strict on staging, blockers only in production
Fail staging builds on anything high or worse, and production only on blockers. With both addresses in valiqai.json:
$ valiqai scan --fail-on high # staging, the default environment$ valiqai scan --env production --fail-on blockerScan after a Vercel or Netlify deploy
ValiqAI scans sites on domains you've verified, so scan your own domain, not a *.vercel.app or *.netlify.app address, which you can't verify. Give your staging branch a domain of yours:
- Vercel: assign a domain such as
staging.example.comto thestagingbranch (Project › Settings › Domains). - Netlify: set a branch subdomain on your custom domain (Domain management › Branch subdomains).
Both report deployments to GitHub, so a workflow can wait for the deploy to finish:
name: Scan after deploy
on: deployment_status
jobs:
valiqai:
if: github.event.deployment_status.state == 'success' && github.event.deployment.environment == 'Preview'
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npx -y @valiqai/cli scan https://staging.example.com --fail-on blocker
env:
VALIQAI_API_KEY: ${{ secrets.VALIQAI_API_KEY }}Match the environment name to what your host reports. A deployment behind Vercel's or Netlify's password protection is refused before it starts (VQ711): scan one with protection off.
Post results to Slack
The simplest way is ValiqAI's Slack integration (Integrations › Slack), which posts scan results to a channel you pick.
To post your own message instead, point a webhook at a small function that checks the signature and forwards a line to a Slack incoming webhook:
// An Express app; the same works as a serverless function.
import express from 'express';
import { verifyValiqaiSignature } from './verify.js'; // from the webhooks page
const app = express();
app.post('/hooks/valiqai', express.raw({ type: 'application/json' }), async (req, res) => {
const raw = req.body.toString('utf8');
if (!verifyValiqaiSignature(raw, req.get('X-ValiqAI-Signature') ?? '', process.env.VALIQAI_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const { event, data } = JSON.parse(raw);
const text =
event === 'scan.completed'
? `ValiqAI: ${data.url} ${data.status}, score ${data.score}. https://www.valiqai.com/runs/${data.id}`
: `ValiqAI: the scan of ${data.url} didn't finish (${data.error}).`;
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
res.sendStatus(200);
});
app.listen(3000);Subscribe the webhook to scan.completed and scan.failed, and answer within 10 seconds, or the delivery is tried again.
Use --json in a script
--json prints one result on stdout and the progress on stderr, so the result pipes cleanly:
#!/usr/bin/env bash
set -uo pipefail
result=$(npx -y @valiqai/cli scan https://staging.example.com --json --fail-on none)
blockers=$(echo "$result" | jq '.counts.blocker')
echo "Score $(echo "$result" | jq '.score'), $blockers blocker(s): $(echo "$result" | jq -r '.runUrl')"
if [ "$blockers" -gt 0 ]; then
echo "$result" | jq -r '.problems[] | select(.status == "open" and .severity == "blocker") | "- \(.title)"'
exit 1
fi--fail-on none keeps the exit code at 0 for a finished scan, so the script decides. The fields are in JSON output.
Scan several sites in a matrix
One job per site. A workspace runs one scan at a time, so the jobs take turns: each waits for the one before, up to --timeout. Running them one after another keeps that wait short:
jobs:
valiqai:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 1
matrix:
url:
- https://staging.example.com
- https://staging.shop.example.com
- https://docs.example.com
steps:
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npx -y @valiqai/cli scan "${{ matrix.url }}" --fail-on blocker --timeout 30m
env:
VALIQAI_API_KEY: ${{ secrets.VALIQAI_API_KEY }}Every domain must be verified: by you for a personal key, or by anyone on the team for a team key.