Examples
The component pages each cover one building block in isolation. This page does the opposite: complete flows, built the way you would build them in an app. Every example is runnable, so open the Code tab to see the whole thing, positions and wiring included.
Automations
Routing rules
A trigger pill heads the flow, numbered markers run down the trunk, and every condition fans out to a labeled branch. This is the shape most automation builders end up with.
<template>
<FluxFlow :padding="21">
<FluxFlowNode id="trigger" :x="-4" :y="0">
<FluxFlowPill color="info" icon="bolt" label="Order Released"/>
</FluxFlowNode>
<FluxFlowNode id="step-1" :x="0" :y="103">
<FluxFlowStep :value="1"/>
</FluxFlowNode>
<FluxFlowNode id="condition-1" :x="90" :y="90">
<FluxFlowConditionCard>
US Shopify orders with matching state or SKU
</FluxFlowConditionCard>
</FluxFlowNode>
<FluxFlowNode id="fulfilment-1" :x="270" :y="249">
<FluxFlowActionCard>
Set the order's fulfilment profile to Prestons AU
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowNode id="hold-1" :x="270" :y="411">
<FluxFlowActionCard>
Apply a manual hold
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowNode id="step-2" :x="0" :y="562">
<FluxFlowStep :value="2"/>
</FluxFlowNode>
<FluxFlowNode id="condition-2" :x="90" :y="549">
<FluxFlowConditionCard>
EU orders shipping from the Rotterdam warehouse
</FluxFlowConditionCard>
</FluxFlowNode>
<FluxFlowNode id="fulfilment-2" :x="270" :y="711">
<FluxFlowActionCard>
Set delivery to one day delivery
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowConnection from="trigger" to="step-1" from-align="start"/>
<FluxFlowConnection from="step-1" to="condition-1" to-align="start" marker-start="none"/>
<FluxFlowConnection from="step-1" to="step-2" marker-start="none"/>
<FluxFlowConnection from="step-2" to="condition-2" to-align="start" marker-start="none"/>
<FluxFlowConnection from="condition-1" to="fulfilment-1" from-side="bottom" from-align="start" to-side="left" to-align="start" label="If true" label-placement="last-leg"/>
<FluxFlowConnection from="condition-1" to="hold-1" from-side="bottom" from-align="start" to-side="left" to-align="start" label="If false" label-placement="last-leg"/>
<FluxFlowConnection from="condition-2" to="fulfilment-2" from-side="bottom" from-align="start" to-side="left" to-align="start" label="If true" label-placement="last-leg"/>
</FluxFlow>
</template>
<script
setup
lang="ts">
import { FluxFlow, FluxFlowActionCard, FluxFlowConditionCard, FluxFlowConnection, FluxFlowNode, FluxFlowPill, FluxFlowStep } from '@flux-ui/flow';
</script>Onboarding
A chain places the run of steps and wires it up, and a group frames the two that belong together. Only the branch that leaves the chain carries a coordinate of its own.
<template>
<FluxFlow :padding="30">
<!-- Room between the trigger and the group frame that follows it. -->
<FluxFlowChain :gap="90">
<FluxFlowNode id="signup">
<FluxFlowTriggerCard title="Account created" subtitle="Sign-up form"/>
</FluxFlowNode>
<FluxFlowNode id="verify">
<FluxFlowActionCard title="Verify the email" icon="envelope"/>
</FluxFlowNode>
<FluxFlowNode id="profile">
<FluxFlowActionCard title="Complete the profile" icon="user-check"/>
</FluxFlowNode>
<FluxFlowNode id="welcome">
<FluxFlowActionCard title="Send the welcome tour" icon="wand-magic-sparkles"/>
</FluxFlowNode>
</FluxFlowChain>
<FluxFlowGroup
title="Onboarding"
color="primary"
:nodes="['verify', 'profile']"/>
<FluxFlowNode id="bounce" :x="411" :y="280">
<FluxFlowActionCard title="Retry in 24 hours" icon="stopwatch" color="danger"/>
</FluxFlowNode>
<FluxFlowConnection
dashed
from="verify"
to="bounce"
from-side="right"
color="danger"
label="Bounced"/>
</FluxFlow>
</template>
<script
setup
lang="ts">
import { FluxFlow, FluxFlowActionCard, FluxFlowChain, FluxFlowConnection, FluxFlowGroup, FluxFlowNode, FluxFlowTriggerCard } from '@flux-ui/flow';
</script>Deploy pipeline
A CI/CD flow: a push builds an image, tests gate the release, and the outcome branches to a deploy or an alert. Colored and dashed connectors separate the happy path from the failure path.
<template>
<FluxFlow :padding="21">
<FluxFlowNode id="push" :x="0" :y="126">
<FluxFlowTriggerCard title="Code Pushed" icon="code-branch">
Triggered on every push to the main branch.
</FluxFlowTriggerCard>
</FluxFlowNode>
<FluxFlowNode id="build" :x="360" :y="120">
<FluxFlowActionCard title="Build Image" icon="server" color="primary">
<FluxDescriptionList>
<FluxDescriptionItem label="Runner">ubuntu-latest</FluxDescriptionItem>
<FluxDescriptionItem label="Duration">2m 14s</FluxDescriptionItem>
</FluxDescriptionList>
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowNode id="test" :x="720" :y="158">
<FluxFlowConditionCard title="Tests Pass?"/>
</FluxFlowNode>
<FluxFlowNode id="deploy" :x="1080" :y="18">
<FluxFlowActionCard title="Deploy" icon="circle-check" color="success">
Ships the image to production.
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowNode id="notify" :x="1080" :y="246">
<FluxFlowActionCard title="Notify Team" icon="envelope" color="danger">
Posts the failure to the on-call channel.
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowConnection from="push" to="build" from-side="right" to-side="left"/>
<FluxFlowConnection from="build" to="test" from-side="right" to-side="left"/>
<FluxFlowConnection from="test" to="deploy" from-side="right" to-side="left" label="Pass" color="success"/>
<FluxFlowConnection from="test" to="notify" from-side="right" to-side="left" label="Fail" color="danger" dashed/>
</FluxFlow>
</template>
<script
setup
lang="ts">
import { FluxDescriptionItem, FluxDescriptionList } from '@flux-ui/components';
import { FluxFlow, FluxFlowActionCard, FluxFlowConditionCard, FluxFlowConnection, FluxFlowNode, FluxFlowTriggerCard } from '@flux-ui/flow';
</script>Knowledge graph
Data sources feeding structured skills that compile into an output. It combines card rows, colored and dashed connectors and endpoint markers into one dense canvas.
<template>
<FluxFlow :padding="21">
<FluxFlowNode id="slack" :x="0" :y="0">
<FluxFlowCard title="Slack" subtitle="1,204 threads"/>
</FluxFlowNode>
<FluxFlowNode id="gmail" :x="0" :y="120">
<FluxFlowCard title="Gmail" subtitle="Support inbox"/>
</FluxFlowNode>
<FluxFlowNode id="zendesk" :x="0" :y="240">
<FluxFlowCard title="Zendesk" subtitle="8,612 tickets"/>
</FluxFlowNode>
<FluxFlowNode id="postgres" :x="0" :y="360">
<FluxFlowCard title="Postgres" subtitle="pricing_db"/>
</FluxFlowNode>
<FluxFlowNode id="notion" :x="0" :y="480">
<FluxFlowCard title="Notion" subtitle="Stale · 38d"/>
</FluxFlowNode>
<FluxFlowNode id="refund" :x="390" :y="74">
<FluxFlowCard title="Refund handling" active>
<FluxDescriptionList>
<FluxDescriptionItem label="Status">
<FluxBadge label="Current" color="success" dot/>
</FluxDescriptionItem>
<FluxDescriptionItem label="Sources">4</FluxDescriptionItem>
<FluxDescriptionItem label="Used by">3 agents</FluxDescriptionItem>
</FluxDescriptionList>
</FluxFlowCard>
</FluxFlowNode>
<FluxFlowNode id="pricing" :x="390" :y="374">
<FluxFlowCard title="Pricing exceptions">
<FluxDescriptionList>
<FluxDescriptionItem label="Status">
<FluxBadge label="Needs review" color="warning" dot/>
</FluxDescriptionItem>
<FluxDescriptionItem label="Sources">2</FluxDescriptionItem>
<FluxDescriptionItem label="Used by">4 agents</FluxDescriptionItem>
</FluxDescriptionList>
</FluxFlowCard>
</FluxFlowNode>
<FluxFlowNode id="output" :x="780" :y="219">
<FluxFlowCard title="Skills file">
<FluxStatisticsLegend direction="vertical">
<FluxStatisticsLegendItem color="primary" label="Support agent"/>
<FluxStatisticsLegendItem color="info" label="Billing ops bot"/>
<FluxStatisticsLegendItem color="success" label="On-call copilot"/>
</FluxStatisticsLegend>
</FluxFlowCard>
</FluxFlowNode>
<FluxFlowConnection from="slack" to="refund" from-side="right" to-side="left"/>
<FluxFlowConnection from="gmail" to="refund" from-side="right" to-side="left"/>
<FluxFlowConnection from="zendesk" to="refund" from-side="right" to-side="left"/>
<FluxFlowConnection from="postgres" to="pricing" from-side="right" to-side="left"/>
<FluxFlowConnection from="notion" to="pricing" from-side="right" to-side="left" color="warning" dashed/>
<FluxFlowConnection from="refund" to="output" from-side="right" to-side="left" color="success"/>
<FluxFlowConnection from="pricing" to="output" from-side="right" to-side="left" color="warning" dashed/>
</FluxFlow>
</template>
<script
setup
lang="ts">
import { FluxBadge, FluxDescriptionItem, FluxDescriptionList } from '@flux-ui/components';
import { FluxFlow, FluxFlowCard, FluxFlowConnection, FluxFlowNode } from '@flux-ui/flow';
import { FluxStatisticsLegend, FluxStatisticsLegendItem } from '@flux-ui/statistics';
</script>Branching on ports
A fraud check whose two outcomes each carry their own port, so a branch leaves the card at the answer it belongs to. Terminals open and close the run, and a note explains the manual step.
<template>
<FluxFlow :padding="21">
<FluxFlowNode id="start" :x="60" :y="0">
<FluxFlowTerminal icon="play" label="Payment received" color="success"/>
</FluxFlowNode>
<FluxFlowNode id="check" :x="0" :y="120">
<FluxFlowConditionCard title="Fraud check" subtitle="Score above 0.8">
<div style="display: flex; align-items: center; justify-content: space-between">
<FluxBadge color="success" icon="circle-check" label="Clean"/>
<FluxFlowPort id="true" side="right"/>
</div>
<div style="display: flex; align-items: center; justify-content: space-between">
<FluxBadge color="danger" icon="circle-xmark" label="Suspicious"/>
<FluxFlowPort id="false" side="right"/>
</div>
</FluxFlowConditionCard>
</FluxFlowNode>
<FluxFlowNode id="ship" :x="450" :y="60">
<FluxFlowActionCard title="Release for shipping" icon="truck"/>
</FluxFlowNode>
<FluxFlowNode id="hold" :x="450" :y="213">
<FluxFlowActionCard title="Hold for review" icon="bell" color="danger"/>
</FluxFlowNode>
<FluxFlowNode id="note" :x="495" :y="420">
<FluxFlowNote title="Manual step">
A held order waits for the risk team, so nothing downstream runs on it.
</FluxFlowNote>
</FluxFlowNode>
<FluxFlowNode id="done" :x="825" :y="73">
<FluxFlowTerminal icon="circle-check" label="Done"/>
</FluxFlowNode>
<FluxFlowConnection from="start" to="check" marker-start="none"/>
<FluxFlowConnection from="check" from-port="true" to="ship" color="success"/>
<FluxFlowConnection from="check" from-port="false" to="hold" color="danger"/>
<FluxFlowConnection from="ship" to="done" marker-end="none"/>
<FluxFlowConnection from="note" to="hold" to-side="bottom" marker-start="none" marker-end="none" dashed/>
</FluxFlow>
</template>
<script
setup
lang="ts">
import { FluxBadge } from '@flux-ui/components';
import { FluxFlow, FluxFlowActionCard, FluxFlowConditionCard, FluxFlowConnection, FluxFlowNode, FluxFlowNote, FluxFlowPort, FluxFlowTerminal } from '@flux-ui/flow';
</script>Live flows
Nothing about a card is read only. Because the body is plain markup, controls inside it keep working on the canvas, which lets a flow double as the control surface for the process it draws.
Enable and disable
A FluxToggle in the trigger card switches the automation on and off; the downstream cards and connectors follow its state.
<template>
<div style="height: 390px">
<FluxFlow :padding="21" interactive background="dots">
<FluxFlowNode id="trigger" :x="0" :y="90">
<FluxFlowTriggerCard
title="New order"
subtitle="Shopify webhook"
:color="enabled ? 'info' : 'gray'"
:active="enabled">
<FluxDescriptionList>
<FluxDescriptionItem label="Automation">
<FluxToggle v-model="enabled"/>
</FluxDescriptionItem>
</FluxDescriptionList>
</FluxFlowTriggerCard>
</FluxFlowNode>
<FluxFlowNode id="charge" :x="360" :y="0">
<FluxFlowActionCard title="Charge card" icon="money-bill" :color="enabled ? 'primary' : 'gray'">
Captures the payment for the order total.
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowNode id="email" :x="360" :y="216">
<FluxFlowActionCard title="Send receipt" icon="envelope" :color="enabled ? 'info' : 'gray'">
Emails the customer their order confirmation.
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowConnection from="trigger" to="charge" from-side="right" to-side="left" :color="enabled ? 'primary' : undefined" :dashed="!enabled"/>
<FluxFlowConnection from="trigger" to="email" from-side="right" to-side="left" :color="enabled ? 'info' : undefined" :dashed="!enabled"/>
</FluxFlow>
</div>
</template>
<script
setup
lang="ts">
import { FluxDescriptionItem, FluxDescriptionList, FluxToggle } from '@flux-ui/components';
import { FluxFlow, FluxFlowActionCard, FluxFlowConnection, FluxFlowNode, FluxFlowTriggerCard } from '@flux-ui/flow';
import { ref } from 'vue';
const enabled = ref(true);
</script>Approval step
A FluxSecondaryButton in a card footer resolves an approval, which activates the next node and fills its connector.
<template>
<div style="height: 390px">
<FluxFlow :padding="21" interactive background="dots">
<FluxFlowNode id="request" :x="0" :y="60">
<FluxFlowTriggerCard title="Expense submitted" subtitle="€ 640 · Travel"/>
</FluxFlowNode>
<FluxFlowNode id="review" :x="360" :y="13">
<FluxFlowConditionCard
title="Manager approval"
:icon="approved ? 'circle-check' : 'hourglass-clock'"
:color="approved ? 'success' : 'warning'"
:active="!approved">
<FluxDescriptionList>
<FluxDescriptionItem label="Reviewer">
<span style="display: flex; align-items: center; gap: 9px">
<FluxAvatar :size="24" fallback="colorized" fallback-initials="JD"/>
Jane Doe
</span>
</FluxDescriptionItem>
</FluxDescriptionList>
<template #footer>
<FluxSecondaryButton
v-if="!approved"
icon-leading="circle-check"
label="Approve"
@click="approved = true"/>
<FluxBadge
v-else
icon="circle-check"
label="Approved"
color="success"/>
</template>
</FluxFlowConditionCard>
</FluxFlowNode>
<FluxFlowNode id="pay" :x="720" :y="38">
<FluxFlowActionCard title="Reimburse" icon="money-bill" :color="approved ? 'success' : 'gray'" :active="approved">
Transfers the amount to the employee.
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowConnection from="request" to="review" from-side="right" to-side="left" :progress-value="1"/>
<FluxFlowConnection from="review" to="pay" from-side="right" to-side="left" progress-color="success" :progress-value="approved ? 1 : 0"/>
</FluxFlow>
</div>
</template>
<script
setup
lang="ts">
import { FluxAvatar, FluxBadge, FluxDescriptionItem, FluxDescriptionList, FluxSecondaryButton } from '@flux-ui/components';
import { FluxFlow, FluxFlowActionCard, FluxFlowConditionCard, FluxFlowConnection, FluxFlowNode, FluxFlowTriggerCard } from '@flux-ui/flow';
import { ref } from 'vue';
const approved = ref(false);
</script>Run on demand
A FluxPrimaryButton kicks off a run: a FluxStatisticsMeter animates the build while the connectors fill from stage to stage.
<template>
<div style="height: 390px">
<FluxFlow :padding="21" interactive background="dots">
<FluxFlowNode id="trigger" :x="0" :y="60">
<FluxFlowTriggerCard title="Deploy requested" subtitle="main branch">
<template #footer>
<FluxPrimaryButton
icon-leading="play"
:label="running ? 'Running' : 'Run'"
:is-loading="running"
:disabled="running"
@click="run"/>
</template>
</FluxFlowTriggerCard>
</FluxFlowNode>
<FluxFlowNode id="build" :x="360" :y="78">
<FluxFlowActionCard title="Build image" icon="server" color="primary" :active="building">
<FluxStatisticsMeter
color="primary"
icon="gauge-high"
title="Progress"
:value="build"/>
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowNode id="ship" :x="720" :y="74">
<FluxFlowActionCard title="Ship to production" icon="circle-check" color="success" :active="done">
Rolls the new image out to production.
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowConnection from="trigger" to="build" from-side="right" to-side="left" progress-color="primary" :progress-value="handoff"/>
<FluxFlowConnection from="build" to="ship" from-side="right" to-side="left" progress-color="success" :progress-value="release"/>
</FluxFlow>
</div>
</template>
<script
setup
lang="ts">
import { FluxPrimaryButton } from '@flux-ui/components';
import { FluxFlow, FluxFlowActionCard, FluxFlowConnection, FluxFlowNode, FluxFlowTriggerCard } from '@flux-ui/flow';
import { FluxStatisticsMeter } from '@flux-ui/statistics';
import { computed, onBeforeUnmount, ref } from 'vue';
// A run walks three stages in order: the connector fills towards the build,
// the build itself runs, and only then does the release connector fill.
const STAGES = 3;
const STAGE_DURATION = 800;
const elapsed = ref(0);
const running = ref(false);
const handoff = computed(() => clamp(elapsed.value));
const build = computed(() => clamp(elapsed.value - 1));
const release = computed(() => clamp(elapsed.value - 2));
const building = computed(() => elapsed.value > 1 && elapsed.value < 2);
const done = computed(() => elapsed.value >= STAGES);
let frame = 0;
onBeforeUnmount(() => cancelAnimationFrame(frame));
function clamp(value: number): number {
return Math.min(Math.max(value, 0), 1);
}
function run(): void {
cancelAnimationFrame(frame);
running.value = true;
elapsed.value = 0;
const start = performance.now();
const tick = (now: number) => {
elapsed.value = Math.min((now - start) / STAGE_DURATION, STAGES);
if (elapsed.value < STAGES) {
frame = requestAnimationFrame(tick);
} else {
running.value = false;
}
};
frame = requestAnimationFrame(tick);
}
</script>Running pipeline
A nightly sync walks its own stages: the step that is busy carries a spinner in place of its icon, its connector fills while it runs, and a finished stage flips to a check and a Done badge.
<template>
<FluxFlow :padding="21">
<FluxFlowNode id="trigger" :x="81" :y="0">
<FluxFlowPill
color="info"
icon="bolt"
label="Nightly sync"
:is-loading="isRunning(0)"/>
</FluxFlowNode>
<FluxFlowNode
v-for="(stage, index) of STAGES"
:key="stage.id"
:id="stage.id"
:x="0"
:y="120 + index * 270">
<FluxFlowActionCard
:title="stage.title"
:subtitle="stage.subtitle"
:icon="isDone(index + 1) ? 'circle-check' : stage.icon"
:color="colorOf(index + 1)"
:active="isRunning(index + 1)"
:is-loading="isRunning(index + 1)">
{{ stage.description }}
<template #footer>
<FluxBadge
:color="colorOf(index + 1)"
:label="labelOf(index + 1)"/>
</template>
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowConnection
from="trigger"
to="fetch"
from-align="start"
progress-color="primary"
:progress-value="progressOf(1)"/>
<FluxFlowConnection
from="fetch"
to="enrich"
progress-color="primary"
:progress-value="progressOf(2)"/>
<FluxFlowConnection
from="enrich"
to="load"
progress-color="primary"
:progress-value="progressOf(3)"/>
</FluxFlow>
</template>
<script
setup
lang="ts">
import type { FluxColor, FluxIconName } from '@flux-ui/types';
import { FluxBadge } from '@flux-ui/components';
import { FluxFlow, FluxFlowActionCard, FluxFlowConnection, FluxFlowNode, FluxFlowPill } from '@flux-ui/flow';
import { onBeforeUnmount, onMounted, ref } from 'vue';
const STAGES: { readonly id: string; readonly icon: FluxIconName; readonly title: string; readonly subtitle: string; readonly description: string }[] = [
{id: 'fetch', icon: 'database', title: 'Fetch orders', subtitle: 'Shopify API', description: 'Pulls every order created since the last successful run.'},
{id: 'enrich', icon: 'wand-magic-sparkles', title: 'Enrich records', subtitle: 'Customer service', description: 'Adds customer, currency and tax data to each order.'},
{id: 'load', icon: 'server', title: 'Load into warehouse', subtitle: 'BigQuery', description: 'Writes the enriched batch and marks the run as complete.'}
];
// The run walks the pill and the three cards in order: a step runs for one
// unit of elapsed time, during which the connector towards the next step
// fills, so a step that starts running is already fully connected.
const STEPS = STAGES.length + 1;
const STEP_DURATION = 1400;
// A finished run holds for a beat, then the canvas clears for a beat before
// the next one starts, so the reset reads as a reset.
const HOLD = 1;
const IDLE = 0.6;
const elapsed = ref(0);
let frame = 0;
onMounted(() => {
const start = performance.now();
const tick = (now: number) => {
elapsed.value = (((now - start) / STEP_DURATION) % (STEPS + HOLD + IDLE)) - IDLE;
frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
});
onBeforeUnmount(() => cancelAnimationFrame(frame));
function isRunning(step: number): boolean {
return elapsed.value >= step && elapsed.value < step + 1;
}
function isDone(step: number): boolean {
return elapsed.value >= step + 1;
}
function colorOf(step: number): FluxColor {
if (isDone(step)) {
return 'success';
}
return isRunning(step) ? 'primary' : 'gray';
}
function labelOf(step: number): string {
if (isDone(step)) {
return 'Done';
}
return isRunning(step) ? 'Running' : 'Pending';
}
function progressOf(step: number): number {
return Math.min(Math.max(elapsed.value - step + 1, 0), 1);
}
</script>