In questo articolo fortemente tecnico capiremo come inviare un workflow e un prompt a ComfyUI tramite PHP. Prima di tutto abbiamo la necessità di impostare un ecosistema server in locale per far girare PHP. Darò per scontato che il mio lettore sia un programmatore PHP di livello intermedio.
Il mio ambiente preferito per lo sviluppo in PHP è Laragon. All’interno della cartella C>laragon>www>comfyui>www posizionerò i file:

Il ruolo di questi file sarà:
index.php
È l’interfaccia web. Mostra le textarea per prompt positivo e negativo, invia i dati agenerate.php, controlla lo stato della generazione e mostra l’immagine finale.
generate.php
È il backend che prende i prompt dal form, leggeworkflow_api.json, sostituisce i testi nei nodi giusti, converte il workflow nel formato API di ComfyUI e lo invia a ComfyUI conPOST /prompt.
status.php
Controlla se la generazione è finita. Interrogahistory/{prompt_id}, legge le immagini prodotte e costruisce gli URL/viewper mostrarle nel browser.
workflow_api.json
È il workflow di ComfyUI: descrive i nodi, i collegamenti e i parametri della pipeline di generazione. In pratica è il “template” tecnico che la tua app usa per dire a ComfyUI cosa eseguire.
Avviamo ComfyUI e verifichiamo il punto di esposizione delle API che sarà in questo caso http://127.0.0.1:8000

Lasciamo l’are di lavoro, vuota, non serve caricare nessun Worflow perché quello sarà inviato a ComfyUI tramite API.

Passiamo al codice dell’applicazione.
workflow_api.json
{
"id": "bf11bf13-0fba-4b6b-9a8d-1680bf10cb09",
"revision": 0,
"last_node_id": 31,
"last_link_id": 66,
"nodes": [
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
424.4159660965347,
639.6974018396478
],
"size": [
378,
144
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
55
]
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
292.7442742020039,
348.82157427473624
],
"size": [
510.328125,
216.71875
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 39
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
56
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
},
{
"id": 14,
"type": "KSamplerSelect",
"pos": [
544.3999868532744,
-183.43982912695446
],
"size": [
378,
82.65625
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "SAMPLER",
"type": "SAMPLER",
"links": [
59
]
}
],
"properties": {
"Node name for S&R": "KSamplerSelect"
},
"widgets_values": [
"euler_ancestral_cfg_pp"
]
},
{
"id": 22,
"type": "SDTurboScheduler",
"pos": [
544.315563264503,
-343.0669877968183
],
"size": [
378,
112
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 45
}
],
"outputs": [
{
"name": "SIGMAS",
"type": "SIGMAS",
"slot_index": 0,
"links": [
60
]
}
],
"properties": {
"Node name for S&R": "SDTurboScheduler"
},
"widgets_values": [
1,
1
]
},
{
"id": 20,
"type": "CheckpointLoaderSimple",
"pos": [
-226.47404675674875,
-72.85890189522229
],
"size": [
412.4375,
130.65625
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": [
45,
58
]
},
{
"name": "CLIP",
"type": "CLIP",
"slot_index": 1,
"links": [
38,
39
]
},
{
"name": "VAE",
"type": "VAE",
"slot_index": 2,
"links": [
63
]
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple",
"models": [
{
"name": "sd_xl_turbo_1.0_fp16.safetensors",
"url": "https://huggingface.co/stabilityai/sdxl-turbo/resolve/main/sd_xl_turbo_1.0_fp16.safetensors",
"directory": "checkpoints"
}
]
},
"widgets_values": [
"sd_xl_turbo_1.0_fp16.safetensors"
]
},
{
"id": 30,
"type": "SaveImage",
"pos": [
1927.2175348197215,
-129.1549547157644
],
"size": [
872.4375,
1013.765625
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 65
}
],
"outputs": [],
"properties": {},
"widgets_values": [
"api_call_"
]
},
{
"id": 31,
"type": "VAEDecode",
"pos": [
1565.3525469788833,
103.67263233712231
],
"size": [
252,
72
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 64
},
{
"name": "vae",
"type": "VAE",
"link": 63
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
65
]
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 29,
"type": "SamplerCustom",
"pos": [
1049.759589239149,
40.138958284003934
],
"size": [
426.234375,
292
],
"flags": {},
"order": 6,
"mode": 0,
"showAdvanced": true,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 58
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 57
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 56
},
{
"name": "sampler",
"type": "SAMPLER",
"link": 59
},
{
"name": "sigmas",
"type": "SIGMAS",
"link": 60
},
{
"name": "latent_image",
"type": "LATENT",
"link": 55
}
],
"outputs": [
{
"name": "output",
"type": "LATENT",
"slot_index": 0,
"links": [
64
]
},
{
"name": "denoised_output",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "SamplerCustom"
},
"widgets_values": [
true,
166912596748480,
"randomize",
1
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
297.42913173876855,
91.39986083838619
],
"size": [
507.40625,
197.171875
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 38
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
57
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
}
],
"links": [
[
38,
20,
1,
6,
0,
"CLIP"
],
[
39,
20,
1,
7,
0,
"CLIP"
],
[
45,
20,
0,
22,
0,
"MODEL"
],
[
55,
5,
0,
29,
5,
"LATENT"
],
[
56,
7,
0,
29,
2,
"CONDITIONING"
],
[
57,
6,
0,
29,
1,
"CONDITIONING"
],
[
58,
20,
0,
29,
0,
"MODEL"
],
[
59,
14,
0,
29,
3,
"SAMPLER"
],
[
60,
22,
0,
29,
4,
"SIGMAS"
],
[
63,
20,
2,
31,
1,
"VAE"
],
[
64,
29,
0,
31,
0,
"LATENT"
],
[
65,
31,
0,
30,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.8429752066115733,
"offset": [
1193.2480222038573,
622.6618163837708
]
},
"frontendVersion": "1.39.19",
"workflowRendererVersion": "Vue"
},
"version": 0.4
}index.php
<?php
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<title>ComfyUI Web Interface</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: Arial, sans-serif;
max-width: 900px;
margin: 30px auto;
padding: 0 16px;
background: #f7f7f7;
color: #222;
}
.card {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,.08);
}
label {
display: block;
margin: 14px 0 6px;
font-weight: bold;
}
textarea {
width: 100%;
min-height: 120px;
padding: 12px;
font-size: 15px;
border: 1px solid #ccc;
border-radius: 8px;
resize: vertical;
box-sizing: border-box;
}
button {
margin-top: 18px;
padding: 12px 18px;
border: 0;
border-radius: 8px;
background: #2563eb;
color: #fff;
font-size: 15px;
cursor: pointer;
}
button:disabled {
opacity: .6;
cursor: not-allowed;
}
.status {
margin-top: 18px;
padding: 12px;
border-radius: 8px;
background: #eef2ff;
white-space: pre-wrap;
}
.error {
background: #fee2e2;
color: #991b1b;
}
.success {
background: #dcfce7;
color: #166534;
}
.preview {
margin-top: 20px;
}
.preview img {
max-width: 100%;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,.12);
}
</style>
</head>
<body>
<div class="card">
<h1>Generatore immagini con ComfyUI</h1>
<label for="positive">Prompt positivo</label>
<textarea id="positive" placeholder="Descrivi quello che vuoi generare..."></textarea>
<label for="negative">Prompt negativo</label>
<textarea id="negative" placeholder="Cosa vuoi evitare..."></textarea>
<button id="generateBtn">Genera immagine</button>
<div id="status" class="status" style="display:none;"></div>
<div id="preview" class="preview"></div>
</div>
<script>
const generateBtn = document.getElementById('generateBtn');
const positiveEl = document.getElementById('positive');
const negativeEl = document.getElementById('negative');
const statusEl = document.getElementById('status');
const previewEl = document.getElementById('preview');
function setStatus(message, type = '') {
statusEl.style.display = 'block';
statusEl.className = 'status ' + type;
statusEl.textContent = message;
}
async function pollStatus(promptId) {
let attempts = 0;
const maxAttempts = 120;
while (attempts < maxAttempts) {
attempts++;
const res = await fetch('status.php?prompt_id=' + encodeURIComponent(promptId));
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Errore durante il controllo stato');
}
if (data.done) {
setStatus('Generazione completata.', 'success');
if (data.images && data.images.length > 0) {
previewEl.innerHTML = data.images.map(img =>
`<div><img src="${img.url}" alt="Immagine generata"></div>`
).join('');
} else {
previewEl.innerHTML = '<p>Nessuna immagine trovata nella history.</p>';
}
return;
}
setStatus('ComfyUI sta generando... tentativo ' + attempts);
await new Promise(resolve => setTimeout(resolve, 2000));
}
throw new Error('Timeout: la generazione non è terminata in tempo.');
}
generateBtn.addEventListener('click', async () => {
previewEl.innerHTML = '';
generateBtn.disabled = true;
try {
setStatus('Invio workflow a ComfyUI...');
const formData = new FormData();
formData.append('positive', positiveEl.value);
formData.append('negative', negativeEl.value);
const res = await fetch('generate.php', {
method: 'POST',
body: formData
});
const data = await res.json();
if (!res.ok) {
const details = data.details
? (typeof data.details === 'string'
? data.details
: JSON.stringify(data.details, null, 2))
: '';
throw new Error((data.error || 'Errore durante l\'invio a ComfyUI') + (details ? '\n\n' + details : ''));
}
if (!data.prompt_id) {
throw new Error('ComfyUI non ha restituito prompt_id');
}
setStatus('Workflow inviato. Prompt ID: ' + data.prompt_id);
await pollStatus(data.prompt_id);
} catch (err) {
setStatus(err.message, 'error');
} finally {
generateBtn.disabled = false;
}
});
</script>
</body>
</html>generate.php
<?php
header('Content-Type: application/json; charset=utf-8');
$comfyBase = 'http://127.0.0.1:8000';
$workflowPath = __DIR__ . '/workflow_api.json';
$positive = trim($_POST['positive'] ?? '');
$negative = trim($_POST['negative'] ?? '');
if ($positive === '') {
http_response_code(400);
echo json_encode(['error' => 'Il prompt positivo è obbligatorio.'], JSON_UNESCAPED_UNICODE);
exit;
}
if (!file_exists($workflowPath)) {
http_response_code(500);
echo json_encode(['error' => 'workflow_api.json non trovato in: ' . $workflowPath], JSON_UNESCAPED_UNICODE);
exit;
}
$raw = file_get_contents($workflowPath);
$workflow = json_decode($raw, true);
if ($workflow === null) {
http_response_code(500);
echo json_encode(['error' => 'workflow_api.json non è un JSON valido.'], JSON_UNESCAPED_UNICODE);
exit;
}
if (!isset($workflow['nodes']) || !is_array($workflow['nodes'])) {
http_response_code(500);
echo json_encode(['error' => 'Formato workflow non valido: manca "nodes".'], JSON_UNESCAPED_UNICODE);
exit;
}
$linksById = [];
if (!empty($workflow['links']) && is_array($workflow['links'])) {
foreach ($workflow['links'] as $link) {
$linksById[$link[0]] = $link;
}
}
$prompt = [];
foreach ($workflow['nodes'] as $node) {
$nodeId = (string)$node['id'];
$inputs = [];
if (!empty($node['inputs']) && is_array($node['inputs'])) {
foreach ($node['inputs'] as $input) {
if (isset($input['link']) && $input['link'] !== null) {
$linkId = $input['link'];
if (isset($linksById[$linkId])) {
$link = $linksById[$linkId];
$fromNodeId = (string)$link[1];
$fromSlot = (int)$link[2];
$inputs[$input['name']] = [$fromNodeId, $fromSlot];
}
}
}
}
$widgets = $node['widgets_values'] ?? [];
switch ($node['type']) {
case 'CLIPTextEncode':
$inputs['text'] = $widgets[0] ?? '';
break;
case 'EmptyLatentImage':
$inputs['width'] = (int)($widgets[0] ?? 512);
$inputs['height'] = (int)($widgets[1] ?? 512);
$inputs['batch_size'] = (int)($widgets[2] ?? 1);
break;
case 'CheckpointLoaderSimple':
$inputs['ckpt_name'] = $widgets[0] ?? '';
break;
case 'KSamplerSelect':
$inputs['sampler_name'] = $widgets[0] ?? 'euler';
break;
case 'SDTurboScheduler':
$inputs['steps'] = (int)($widgets[0] ?? 1);
$inputs['denoise'] = (float)($widgets[1] ?? 1);
break;
case 'SamplerCustom':
$inputs['add_noise'] = (bool)($widgets[0] ?? true);
$inputs['noise_seed'] = (int)($widgets[1] ?? 0);
$inputs['cfg'] = (float)($widgets[3] ?? 1);
break;
case 'SaveImage':
$inputs['filename_prefix'] = $widgets[0] ?? 'ComfyUI';
break;
}
$prompt[$nodeId] = [
'class_type' => $node['type'],
'inputs' => $inputs,
];
}
if (!isset($prompt['6'])) {
http_response_code(500);
echo json_encode(['error' => 'Nodo 6 non trovato nel workflow.'], JSON_UNESCAPED_UNICODE);
exit;
}
if (!isset($prompt['7'])) {
http_response_code(500);
echo json_encode(['error' => 'Nodo 7 non trovato nel workflow.'], JSON_UNESCAPED_UNICODE);
exit;
}
$prompt['6']['inputs']['text'] = $positive;
$prompt['7']['inputs']['text'] = $negative;
$payload = json_encode([
'prompt' => $prompt,
'client_id' => uniqid('laragon_', true)
], JSON_UNESCAPED_UNICODE);
if ($payload === false) {
http_response_code(500);
echo json_encode(['error' => 'Errore nella serializzazione JSON del payload.'], JSON_UNESCAPED_UNICODE);
exit;
}
$ch = curl_init($comfyBase . '/prompt');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
if ($response === false) {
http_response_code(500);
echo json_encode([
'error' => 'Errore cURL verso ComfyUI: ' . curl_error($ch)
], JSON_UNESCAPED_UNICODE);
exit;
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true);
if ($httpCode >= 400) {
http_response_code($httpCode);
echo json_encode([
'error' => 'ComfyUI ha restituito un errore.',
'http_code' => $httpCode,
'details' => $data ?: $response,
'sent_prompt' => $prompt
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;
}
echo json_encode($data ?: ['raw' => $response], JSON_UNESCAPED_UNICODE);status.php
<?php
header('Content-Type: application/json');
$comfyBase = 'http://127.0.0.1:8000';
$promptId = $_GET['prompt_id'] ?? '';
if ($promptId === '') {
http_response_code(400);
echo json_encode(['error' => 'prompt_id mancante.']);
exit;
}
$ch = curl_init($comfyBase . '/history/' . urlencode($promptId));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
]);
$response = curl_exec($ch);
if ($response === false) {
http_response_code(500);
echo json_encode(['error' => 'Errore cURL verso ComfyUI: ' . curl_error($ch)]);
exit;
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true);
if ($httpCode >= 400 || !is_array($data)) {
http_response_code(500);
echo json_encode([
'error' => 'Risposta non valida da ComfyUI.',
'details' => $response
]);
exit;
}
if (!isset($data[$promptId])) {
echo json_encode([
'done' => false,
'images' => []
]);
exit;
}
$entry = $data[$promptId];
$images = [];
if (isset($entry['outputs']) && is_array($entry['outputs'])) {
foreach ($entry['outputs'] as $nodeId => $output) {
if (isset($output['images']) && is_array($output['images'])) {
foreach ($output['images'] as $img) {
$filename = $img['filename'] ?? '';
$subfolder = $img['subfolder'] ?? '';
$type = $img['type'] ?? 'output';
$url = $comfyBase . '/view?filename=' . urlencode($filename)
. '&subfolder=' . urlencode($subfolder)
. '&type=' . urlencode($type);
$images[] = [
'filename' => $filename,
'subfolder' => $subfolder,
'type' => $type,
'url' => $url
];
}
}
}
}
echo json_encode([
'done' => !empty($images),
'images' => $images
]);A grandi linee funziona così:
ComfyUI non ragiona in termini di “campi del form”, ma in termini di nodi del workflow. Ogni nodo ha un ID, per esempio 6 o 7, che lo identifica dentro il JSON del workflow.
Nel tuo caso il backend fa questo passaggio:
- legge
workflow_api.json - trova i nodi con gli ID che ti interessano
- modifica i valori giusti dentro quei nodi
- invia il workflow aggiornato a ComfyUI
Come “agganciamo” il punto giusto
Nel workflow ci sono nodi diversi: caricamento modello, prompt positivo, prompt negativo, sampler, salvataggio immagine, ecc.
Per esempio:
- nodo
6= prompt positivo - nodo
7= prompt negativo
Quindi nel PHP facciamo qualcosa del tipo:
$prompt['6']['inputs']['text'] = $positive;
$prompt['7']['inputs']['text'] = $negative;
Qui l’ID del nodo serve come “indirizzo” per dire:
- questo testo va nel nodo del prompt positivo
- quest’altro va nel nodo del prompt negativo
In pratica non cerchiamo “una textarea nel workflow”, ma un nodo preciso per ID.
Cosa succede dopo
Dopo aver aggiornato quei nodi, il backend manda tutto a ComfyUI con POST /prompt.
A quel punto ComfyUI:
- riceve il workflow completo
- legge i collegamenti tra i nodi
- esegue il grafo nell’ordine corretto
- passa i dati da un nodo all’altro
Per esempio, in modo semplificato:
- il nodo checkpoint carica il modello
- il nodo prompt positivo codifica il testo positivo
- il nodo prompt negativo codifica il testo negativo
- il sampler usa modello + prompt + latent
- il nodo finale salva l’immagine
Perché servono anche i link tra nodi
Non basta sapere l’ID del nodo: serve anche sapere come i nodi sono collegati.
Il workflow contiene infatti:
- i nodi
- i link
I link dicono cose tipo:
- l’output del nodo 20 entra nel nodo 6
- l’output del nodo 6 entra nel sampler
- l’output del sampler entra nel decoder
- il decoder entra nel save image
Quindi gli ID servono per:
- identificare il nodo corretto da modificare
- ricostruire i collegamenti tra i pezzi del workflow
Flusso completo
Riassunto del ciclo:
index.phpraccoglie il promptgenerate.phpapre il workflow JSON- cerca i nodi giusti tramite ID
- sostituisce i valori dei campi utili
- invia il workflow a ComfyUI
- ComfyUI esegue il grafo
- restituisce un
prompt_id status.phpusa quelprompt_idper chiedere a ComfyUI se il job è finito- quando trova l’output, recupera i file immagine
Differenza tra node id e prompt_id
Sono due cose diverse:
- node ID: identifica un blocco dentro il workflow, per esempio il nodo del prompt positivo
- prompt_id: identifica una specifica esecuzione del workflow inviata a ComfyUI
Quindi:
- con il node ID decidi dove scrivere i dati nel workflow
- con il prompt_id controlli lo stato del job dopo l’invio
Pensa al workflow come a una catena di montaggio.
- gli ID dei nodi sono i numeri delle singole macchine
- i link sono i nastri trasportatori tra le macchine
- il backend cambia il contenuto di alcune macchine, per esempio il testo del prompt
- ComfyUI avvia la linea
- il
prompt_idè il numero di pratica di quella specifica lavorazione

Se l’articolo ti è piaciuto restiamo in contatto su linkedin a: https://www.linkedin.com/in/andreatonin/
Nerd per passione e per professione da oltre 30 anni, lavoro nel mondo dell’innovazione tecnologica come CTO e consulente, progettando ecosistemi software complessi e scalabili. Parallelamente mi dedico alla formazione informatica, condividendo esperienze e buone pratiche maturate sul campo.
Scopri di più sulla mia attività di consulenza su lucedigitale.com Mi trovi anche su LinkedIn



















