Docs
Getting started

Put the pieces together

View Markdown

A lot of useful apps follow the same pattern: take some data, apply your logic, and give someone a simple way to use the result.

Here, you'll build a small demand planner. A Python function fits a line to eight sample weeks. An API uses that model to estimate orders from a weekly advertising budget. A React form lets someone try a number and see the result.

Demand planner with a $400 weekly budget and an estimate of 84 orders

The dataset is synthetic. This is a demonstration of the application pattern, not a validated business forecast. The interactive HTML preview illustrates the interface locally; the app you build below calls the Python backend.

Why these pieces are separate

The transform fits the model once and publishes its coefficients as a table. The API reads that output to answer each budget request quickly. The React app collects the budget and displays the response. Changing the budget calls the API; it does not retrain the model.

Your coding agent implements these pieces locally and uses the CLI to save, test, and run them. Use the platform alongside it to inspect the model output, check execution results, and later prepare production output before publishing its consumers.

This example keeps its sample data in Python. For your own source data, follow bring in data. Add a managed database if users should save scenarios or decisions; the prediction form itself does not need one.

Create the pieces

Start in an initialized workspace, as in your first app:

vortexify create transform demand_model
vortexify create api predict_demand
vortexify create app planner

Open the workspace in your coding agent. The three starters are ordinary source directories. The transform does the background calculation, the API answers a request, and the app supplies the interface.

Fit a small model

Replace transforms/demand_model/main.py with:

from io import StringIO

import pandas as pd
from vortexify import Input, vxfy_transform


SAMPLE_CSV = """spend,orders
0,30
100,44
200,58
300,69
400,86
500,97
600,113
700,123
"""


@vxfy_transform(inputs={"training_csv": Input(str, default=SAMPLE_CSV)})
def transform(training_csv: str) -> pd.DataFrame:
    data = pd.read_csv(StringIO(training_csv))
    x = data["spend"].astype(float)
    y = data["orders"].astype(float)
    if len(data) < 2 or not x.notna().all() or not y.notna().all():
        raise ValueError("Provide at least two complete observations")
    denominator = ((x - x.mean()) ** 2).sum()
    if denominator <= 0:
        raise ValueError("Training spend must vary")
    slope = ((x - x.mean()) * (y - y.mean())).sum() / denominator
    intercept = y.mean() - slope * x.mean()
    return pd.DataFrame([{
        "intercept": float(intercept),
        "slope": float(slope),
        "sample_count": len(data),
        "min_spend": float(x.min()),
        "max_spend": float(x.max()),
    }])

The two columns are weekly advertising spend and observed orders. We fit a simple linear regression: orders = intercept + slope × spend. The function returns the learned numbers as a one-row table so another function can use them.

The inline CSV keeps setup small; there is no external account to configure. training_csv is a normal runtime input with a default. For a real app, you can replace it with a connected dataset and keep the rest of the pattern.

Replace the generated transforms/demand_model/checks.py with a check for this behavior:

from vortexify.testing import vxfy_mocks, vxfy_test


@vxfy_mocks
def known_line():
    return {"training_csv": "spend,orders\n0,10\n100,30\n200,50\n"}


@vxfy_test(mocks=known_line)
def test_fitted_line(run):
    model = run().iloc[0]
    assert abs(model["intercept"] - 10.0) < 1e-8
    assert abs(model["slope"] - 0.2) < 1e-8

Save, test, and run it:

vortexify commit --type transform --resource demand_model
vortexify test transform demand_model --wait
vortexify run transform demand_model --wait

In the platform, open demand_model → Tables and inspect the published coefficient row. In Playground History, select the matching run to check its version and Processing status.

Platform screenshot placeholder: The completed demand_model run and its output table, with the coefficient columns used by the API. Show the producer version so the connection to saved code is clear.

Answer a prediction request

Replace apis/predict_demand/main.py with:

from math import isfinite

import pandas as pd
from vortexify import Input, TransformOutput, vxfy_api


@vxfy_api(
    "/predict-demand",
    inputs={
        "model": TransformOutput("demand_model"),
        "spend": Input(float, default=400.0),
    },
)
def api(model: pd.DataFrame, spend: float) -> dict:
    if not isfinite(spend) or spend < 0:
        raise ValueError("Spend must be a finite, nonnegative number")
    row = model.iloc[0]
    if not float(row["min_spend"]) <= spend <= float(row["max_spend"]):
        raise ValueError("Use spend within the sample range: 0 to 700")
    prediction = float(row["intercept"]) + float(row["slope"]) * spend
    return {
        "predicted_orders": round(max(0.0, prediction)),
        "sample_count": int(row["sample_count"]),
    }

TransformOutput("demand_model") supplies the model table. Input(float, ...) supplies the user's budget. Fitting happens when you run the transform; a request only calculates a prediction from the saved coefficients.

Replace apis/predict_demand/checks.py with:

import pandas as pd
from vortexify.testing import vxfy_mocks, vxfy_test


@vxfy_mocks
def simple_model():
    return {
        "model": pd.DataFrame([{
            "intercept": 10.0, "slope": 0.2, "sample_count": 3,
            "min_spend": 0.0, "max_spend": 700.0,
        }]),
        "spend": 100.0,
    }


@vxfy_test(mocks=simple_model)
def test_prediction(run):
    assert run() == {"predicted_orders": 30, "sample_count": 3}

Then prepare and try the API:

vortexify commit --type api --resource predict_demand
vortexify test api predict_demand --wait
vortexify build api predict_demand --wait
vortexify run api predict_demand --wait

With the default budget of 400, expect predicted_orders: 84 and sample_count: 8.

Give it a form

The app starter already configures the SDK provider. Replace apps/planner/src/App.tsx with:

import { useState } from "react";
import type { FormEvent } from "react";
import { useVortexify } from "@vortexify/react";


type Prediction = { predicted_orders: number; sample_count: number };

export default function App() {
  const { client, ready } = useVortexify();
  const [spend, setSpend] = useState("400");
  const [result, setResult] = useState<Prediction | null>(null);
  const [pending, setPending] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function predict(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPending(true);
    setError(null);
    setResult(null);
    try {
      const response = await client.runApi<Prediction>("predict_demand", {
        spend: Number(spend),
      });
      setResult(response);
    } catch (cause) {
      setError(cause instanceof Error ? cause.message : "Prediction failed");
    } finally {
      setPending(false);
    }
  }

  return (
    <main className="planner">
      <nav><span className="mark">v.</span><strong>Demand planner</strong>
        <span className="badge">SYNTHETIC DATA</span></nav>
      <header><p className="eyebrow">A SMALL WHAT-IF TOOL</p>
        <h1>Turn a question into<br />something you can use.</h1>
        <p>Try a weekly budget. See what a simple model estimates.</p></header>
      <div className="grid">
        <section className="card">
          <p className="eyebrow">01 / YOUR INPUT</p>
          <h2>Plan a week</h2><p>Change one number and explore the result.</p>
          <form onSubmit={predict}>
            <label htmlFor="spend">Weekly advertising budget</label>
            <div className="money"><span>$</span><input id="spend" type="number"
              min="0" max="700" step="any" required value={spend}
              disabled={pending}
              onChange={(event) => { setSpend(event.target.value); setResult(null); }} /></div>
            <small>Sample range: $0–$700 per week</small>
            <button disabled={!ready || pending}>
              {pending ? "Estimating…" : "Estimate orders →"}
            </button>
          </form>
          {error && <p className="error" role="alert">{error}</p>}
        </section>
        <section className="card result" aria-live="polite" aria-busy={pending}>
          <p className="eyebrow">02 / YOUR ESTIMATE</p>
          <div className="number">{result ? result.predicted_orders : "—"}</div>
          <h2>estimated orders / week</h2>
          <p>A prediction from the sample data, ready for your next decision.</p>
          <div className="facts"><div><strong>{result ? result.sample_count : "8"}</strong>
            <span>sample weeks</span></div><div><strong>Linear</strong><span>regression model</span></div></div>
        </section>
      </div>
      <footer>Small dataset. Python model. An API. Your interface.<br />
        Demonstration only · Synthetic data does not establish real-world accuracy.</footer>
    </main>
  );
}
Style the form to match the preview

Replace apps/planner/src/style.css with:

* { box-sizing: border-box; }
body { margin: 0; background: #f7f8fc; color: #202437; font-family: ui-sans-serif, system-ui, sans-serif; }
.planner { max-width: 1080px; margin: auto; padding: 36px 44px; }
nav { display: flex; align-items: center; gap: 12px; font-size: 16px; }
.mark { display: grid; place-items: center; background: #6653df; color: white; width: 34px; height: 34px; border-radius: 10px; font-size: 23px; font-weight: 750; }
.badge { margin-left: auto; font-size: 10px; letter-spacing: .12em; background: #eeebfb; color: #6153a8; border-radius: 20px; padding: 8px 12px; }
header { margin: 52px 0 32px; }
.eyebrow { font-size: 10px; font-weight: 750; letter-spacing: .17em; color: #7465b7; }
h1 { font-size: clamp(30px, 4vw, 44px); line-height: 1.13; letter-spacing: -.045em; margin: 14px 0; }
p { color: #727789; line-height: 1.65; font-size: 14px; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
.card { padding: 30px; border: 1px solid #e7e8ef; border-radius: 18px; background: white; box-shadow: 0 6px 28px #26273b04; }
h2 { font-size: 23px; letter-spacing: -.025em; margin: 20px 0 4px; }
.card > p:not(.eyebrow) { margin: 7px 0 22px; }
label { display: block; font-size: 12px; font-weight: 650; margin: 23px 0 10px; }
.money { display: flex; align-items: center; border: 1px solid #dadce7; border-radius: 10px; padding: 12px 15px; gap: 12px; }
.money span { color: #9396a5; font-size: 20px; }
input { min-width: 0; width: 100%; border: 0; background: transparent; outline-color: #6653df; font: inherit; font-size: 22px; color: #202437; }
small { display: block; margin: 9px 0 25px; color: #9699a7; font-size: 11px; }
button { border: 0; border-radius: 10px; width: 100%; padding: 15px; background: #6653df; color: white; font-size: 13px; font-weight: 650; cursor: pointer; }
button:disabled { opacity: .55; cursor: wait; }
.result { background: #f1effc; border-color: #e4dff7; }
.number { font-size: 85px; line-height: 1; letter-spacing: -.065em; font-weight: 700; color: #5746c5; margin-top: 26px; }
.result h2 { font-size: 17px; margin: 10px 0; }
.result > p:not(.eyebrow) { font-size: 12px; max-width: 280px; }
.facts { display: flex; gap: 45px; border-top: 1px solid #ddd8ef; padding-top: 20px; margin-top: 20px; }
.facts strong { display: block; font-size: 16px; }
.facts span { display: block; font-size: 11px; color: #8c87a0; margin-top: 4px; }
footer { color: #9194a3; text-align: center; font-size: 11px; line-height: 1.9; padding: 27px 0 0; }
.error { color: #a82d3c; }
@media (max-width: 650px) { .planner { padding: 24px; } .grid { grid-template-columns: 1fr; } header { margin-top: 38px; } .badge { font-size: 8px; } }

Start the app:

cd apps/planner
npm install
vortexify dev

Open the URL printed by Vortexify. Try a budget of 400 and submit: you should see 84 estimated orders. Try 100: the estimate should be 44. The form accepts the sample range of 0–700, and the backend validates it too.

Your browser calls predict_demand; the API reads the model output; the model came from your Python calculation. The React component never needs the fitted coefficients or server credentials.

Check the path through the platform

After trying the form, inspect the transform's successful output and the API's execution result in the platform. The value returned by the API should match the value shown in React. If it does not, check the input budget, saved API version, and model output before changing the interface.

Mock tests verify the supplied sample behavior; the live preview checks that the API can resolve the actual transform output. Publishing uses deployed resources, so the preview result is not the last step.

Make it available

When you're ready to publish, return to the project root:

cd ../..
vortexify commit --type app --resource planner
vortexify deploy transform demand_model --wait

Start the deployed demand_model through the production transform execution service and wait for successful output. The current Playground Run control submits preview work; it is not a production-run selector. The earlier CLI run prepared preview data; deploying a transform selects its production version but does not run it. Then publish its consumers:

vortexify deploy api predict_demand --wait
vortexify deploy app planner --wait

In the platform, open the Apps directory and launch planner. Its resource Overview shows the deployed version; its Preview view is a separate hosted preview.

Adapt the pattern

The same structure works for a pricing calculator, an eligibility check, a capacity estimate, or a data-backed recommendation. Replace the dataset and Python logic, define the inputs your operation needs, and adjust the form. You can use ordinary rules or SQL instead of a model.

With real data, evaluate prediction quality and input limits before treating the estimate as reliable. Add connected data or an assistant only when your application needs them.