curl --request POST \
--url https://app.uselamina.ai/v1/workflows/generate \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"instruction": "<string>",
"baseAppId": "<string>",
"ops": [
{}
],
"name": "<string>",
"visibility": "private",
"brandProfileId": "<string>",
"run": false,
"inputs": {},
"test": false
}
'import requests
url = "https://app.uselamina.ai/v1/workflows/generate"
payload = {
"instruction": "<string>",
"baseAppId": "<string>",
"ops": [{}],
"name": "<string>",
"visibility": "private",
"brandProfileId": "<string>",
"run": False,
"inputs": {},
"test": False
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
instruction: '<string>',
baseAppId: '<string>',
ops: [{}],
name: '<string>',
visibility: 'private',
brandProfileId: '<string>',
run: false,
inputs: {},
test: false
})
};
fetch('https://app.uselamina.ai/v1/workflows/generate', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.uselamina.ai/v1/workflows/generate",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'instruction' => '<string>',
'baseAppId' => '<string>',
'ops' => [
[
]
],
'name' => '<string>',
'visibility' => 'private',
'brandProfileId' => '<string>',
'run' => false,
'inputs' => [
],
'test' => false
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.uselamina.ai/v1/workflows/generate"
payload := strings.NewReader("{\n \"instruction\": \"<string>\",\n \"baseAppId\": \"<string>\",\n \"ops\": [\n {}\n ],\n \"name\": \"<string>\",\n \"visibility\": \"private\",\n \"brandProfileId\": \"<string>\",\n \"run\": false,\n \"inputs\": {},\n \"test\": false\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.uselamina.ai/v1/workflows/generate")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"instruction\": \"<string>\",\n \"baseAppId\": \"<string>\",\n \"ops\": [\n {}\n ],\n \"name\": \"<string>\",\n \"visibility\": \"private\",\n \"brandProfileId\": \"<string>\",\n \"run\": false,\n \"inputs\": {},\n \"test\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.uselamina.ai/v1/workflows/generate")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"instruction\": \"<string>\",\n \"baseAppId\": \"<string>\",\n \"ops\": [\n {}\n ],\n \"name\": \"<string>\",\n \"visibility\": \"private\",\n \"brandProfileId\": \"<string>\",\n \"run\": false,\n \"inputs\": {},\n \"test\": false\n}"
response = http.request(request)
puts response.read_body{
"data": {
"appId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"runUrl": "<string>",
"description": "<string>",
"visibility": "private",
"parameters": [
{}
],
"outputs": [
{}
],
"tier": "deterministic",
"creditsSpent": 123,
"editability": {
"score": 123,
"subscores": {},
"notes": [
"<string>"
]
}
}
}{
"error": "<string>",
"details": [
"<string>"
]
}Generate App
Generate a brand-new app from a plain-language instruction when no existing
app fits (check POST /v1/apps/discover first). A headless LLM planner
assembles a validated workflow graph from the node catalog and auto-publishes
it as a private, immediately-runnable app. The result is a normal app: run it
via POST /v1/apps/{appId}/runs with the returned parameter keys.
Pass baseAppId to EDIT an existing generated app in place: the planner starts
from that app’s current graph and applies the instruction as a targeted change,
keeping the same appId and run history. Requires creator or workspace owner/admin.
curl --request POST \
--url https://app.uselamina.ai/v1/workflows/generate \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"instruction": "<string>",
"baseAppId": "<string>",
"ops": [
{}
],
"name": "<string>",
"visibility": "private",
"brandProfileId": "<string>",
"run": false,
"inputs": {},
"test": false
}
'import requests
url = "https://app.uselamina.ai/v1/workflows/generate"
payload = {
"instruction": "<string>",
"baseAppId": "<string>",
"ops": [{}],
"name": "<string>",
"visibility": "private",
"brandProfileId": "<string>",
"run": False,
"inputs": {},
"test": False
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
instruction: '<string>',
baseAppId: '<string>',
ops: [{}],
name: '<string>',
visibility: 'private',
brandProfileId: '<string>',
run: false,
inputs: {},
test: false
})
};
fetch('https://app.uselamina.ai/v1/workflows/generate', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.uselamina.ai/v1/workflows/generate",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'instruction' => '<string>',
'baseAppId' => '<string>',
'ops' => [
[
]
],
'name' => '<string>',
'visibility' => 'private',
'brandProfileId' => '<string>',
'run' => false,
'inputs' => [
],
'test' => false
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.uselamina.ai/v1/workflows/generate"
payload := strings.NewReader("{\n \"instruction\": \"<string>\",\n \"baseAppId\": \"<string>\",\n \"ops\": [\n {}\n ],\n \"name\": \"<string>\",\n \"visibility\": \"private\",\n \"brandProfileId\": \"<string>\",\n \"run\": false,\n \"inputs\": {},\n \"test\": false\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.uselamina.ai/v1/workflows/generate")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"instruction\": \"<string>\",\n \"baseAppId\": \"<string>\",\n \"ops\": [\n {}\n ],\n \"name\": \"<string>\",\n \"visibility\": \"private\",\n \"brandProfileId\": \"<string>\",\n \"run\": false,\n \"inputs\": {},\n \"test\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.uselamina.ai/v1/workflows/generate")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"instruction\": \"<string>\",\n \"baseAppId\": \"<string>\",\n \"ops\": [\n {}\n ],\n \"name\": \"<string>\",\n \"visibility\": \"private\",\n \"brandProfileId\": \"<string>\",\n \"run\": false,\n \"inputs\": {},\n \"test\": false\n}"
response = http.request(request)
puts response.read_body{
"data": {
"appId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"runUrl": "<string>",
"description": "<string>",
"visibility": "private",
"parameters": [
{}
],
"outputs": [
{}
],
"tier": "deterministic",
"creditsSpent": 123,
"editability": {
"score": 123,
"subscores": {},
"notes": [
"<string>"
]
}
}
}{
"error": "<string>",
"details": [
"<string>"
]
}POST /v1/apps/discover first — if a suitable app already exists, run that instead.
A headless LLM planner reads your instruction, assembles a validated workflow graph from Lamina’s node catalog, and auto-publishes it as a private app. The result is a normal app: run it immediately with POST /v1/apps/{appId}/runs using the returned parameter keys, and share it with POST /v1/apps/{appId}/visibility.
One-shot generate + run
To build and execute in a single call — e.g. text → workflow → video — passrun: true with an inputs map keyed by the app’s parameter keys (media as URLs, options as labels). The response then includes a run object with the runId; poll GET /v1/runs/{runId} for the result. The app is always created; if inputs are invalid or a required input is missing, run.started is false and run.errors lists the problems so you can fix them and run the app directly.
On-brand generation
Generated apps are brand-aware by default. Before planning, Lamina resolves the workspace’s brand DNA — voice, visual anchors, and guardrails — and the planner seeds the generation nodes so the app produces on-brand output without you hand-writing brand instructions into every run. PassbrandProfileId to target a specific brand in a multi-brand workspace; omit it to use the workspace’s active brand. If no brand is configured, generation proceeds brand-agnostic — brand context is an enhancement, never a requirement.
Writing a good instruction
Describe what the app produces, the inputs the user will supply, and the desired output. The more concrete, the better the plan.- Good: “Product photo app: the user uploads a product image and types a background scene; output one 1:1 image.”
- Good: “Voiceover app: the user pastes a script; output an audio voiceover.”
Response
Returns the app schema —appId, name, parameters[] (the inputs to pass to a run), outputs[], and a runUrl. Feed the parameter keys straight into POST /v1/apps/{appId}/runs.
On the planner path the response may also include editability — a { score, subscores, notes[] } object measuring how tweakable the generated app is (parameter clarity, grouping, node granularity, whether a control steers the output). A low score with its notes tells you exactly what to improve; edit the app in place (pass baseAppId plus ops or a fresh instruction) to refine it.
If the planner cannot produce a valid app, the response is a 422 with error and details[] describing the unresolved problems.
Provider
Theprovider field (claude or openai) overrides the server default for a single request — useful for A/B comparison. Omit it to use WFGEN_PROVIDER, which falls back to OpenAI. OpenAI-compatible Moonshot/Kimi targets are configured server-side by orchestration role rather than selected by public request fields.Authorizations
Workspace API key. Prefix: lma_. Example: lma_abc123...
Body
What the app should create (or, with baseAppId, the change to apply), including the user inputs it needs and the desired outputs.
Edit mode. The appId of a generated app to modify in place (same appId, run history preserved). Omit to build a new app.
Edit mode cheap path — explicit edit operations applied deterministically (no planner LLM call, no credit charge). Requires baseAppId; instruction becomes optional. Items are setNodeData / addNode / removeNode / addEdge / removeEdge / addParameter / removeParameter / setParameter.
Optional preferred app name.
App reach after creation.
private, shared, public Planner provider override. Defaults to WFGEN_PROVIDER, then OpenAI.
claude, openai Bake a specific brand profile's voice, visual anchors, and guardrails into the generated app so it produces on-brand output. Omit to use the workspace's active brand.
One-shot: after generating the app, immediately start a run using inputs. The response then includes a run object with the runId — poll GET /v1/runs/{runId}.
Input values keyed by the generated app's parameter key (media as URLs, options as labels). Used only when run is true.
Sandbox mode (or send header X-Lamina-Test: true): validate the request and return a stub app -- no planner LLM call, nothing persisted, no credits charged.
Response
App generated and published
Show child attributes
Show child attributes