#Convert prompt to lobster file

1 messages · Page 1 of 1 (latest)

tacit tulip
#

I have the following prompt, can you convert it to a lobster file

Can you draft an email for all leads in the leads file and email it to me, in the email include the draft for each lead as well as a summary of each lead. Also mention in the draft that we can offer the lead a free health check of their SQL estate. The health check will highlight where money can be saved, for example in terms of SQL licensing, as well as any highlight any performance issues they may have. Send the email to me as HTML

Output Format Rules

  1. Tables: Use a Markdown table for price comparisons (Ticker | Price | Change | Target).
  2. No Code Fences: Do not wrap the email content in html or markdown blocks.
  3. No Naked CSS: Do not include a <style> block. Use only basic HTML tags (<b>, <i>, <br>, <table>) if necessary.
pallid locustBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

dire cipherBOT
#

Yep — here’s a .lobster workflow version. I’d keep the actual send behind an approval gate because emailing is an external side effect.

Save as something like sql-lead-drafts.lobster:

name: sql-lead-drafts
args:
  leads_file:
    default: "./leads.json"
  recipient_email:
    required: true
  sender_name:
    default: ""

steps:
  - id: read_leads
#
    command: "cat {{args.leads_file}}"

  - id: draft_email_html
    command: >
      lead-email-drafter --format html
      --recipient "{{args.recipient_email}}"
      --sender-name "{{args.sender_name}}"
    stdin: $read_leads.stdout

  - id: preview
    command: "cat"
    stdin: $draft_email_html.stdout
    approval: required

  - id: send_email
#
    command: >
      lead-email-send
      --to "{{args.recipient_email}}"
      --subject "SQL estate health check lead drafts"
      --content-type "text/html"
    stdin: $draft_email_html.stdout
    condition: $preview.approved

And the prompt/config that lead-email-drafter should use:

Read the leads provided on stdin.

Create one HTML email addressed to the recipient. The email must contain:
#
1. A concise summary of each lead.
2. A tailored outreach draft for each lead.
3. In every outreach draft, mention that we can offer the lead a free health check of their SQL estate.
4. Explain that the health check will highlight where money can be saved, including SQL licensing, and identify any performance issues they may have.

Output only the HTML email body.
Do not include markdown code fences.
Do not include a <style> block.
Use only basic HTML tags such as <b>, <i>, <br>, <p>, <ul>, <li>, and <table> where needed.
If any price comparison is included, use a Markdown table with columns: Ticker | Price | Change | Target.

One note: Lobster runs shell commands/scripts; it won’t magically know what lead-email-drafter or lead-email-send are unless you implement those or swap them for the email/LLM tooling you already have. If you’re using embedded OpenClaw Lobster, avoid relying on nested openclaw.invoke unless you’ve explicitly wired that up. Docs: https://docs.openclaw.ai/tools/lobster

tacit tulip
#

The leads file is a markdown file and I have agentmail installed on openclaw

dire cipherBOT
#

Got it — if the leads file is Markdown and AgentMail is installed, I’d structure the Lobster file like this. The only bit you may need to adjust is the exact AgentMail command name/flags, because I can’t see an agentmail CLI in this runtime.

name: sql-lead-drafts-from-markdown
args:
  leads_file:
    default: "./leads.md"
  recipient_email:
    required: true
  subject:
    default: "SQL estate health check lead drafts"

steps:
  - id: read_leads_markdown
    command: "cat {{args.leads_file}}"
#
  - id: create_html_email
    command: >
      openclaw infer text
      --json
      --prompt-file ./prompts/sql-lead-drafts-html.md
    stdin: $read_leads_markdown.stdout

  - id: preview_email
    command: "cat"
    stdin: $create_html_email.stdout
    approval: required

  - id: send_via_agentmail
    command: >
      agentmail send
#
      --to "{{args.recipient_email}}"
      --subject "{{args.subject}}"
      --html
    stdin: $create_html_email.stdout
    condition: $preview_email.approved

Then save this as ./prompts/sql-lead-drafts-html.md:

You will receive a Markdown leads file on stdin.

Draft one HTML email to me containing:
1. A summary of each lead.
2. A tailored outreach draft for each lead.
#
3. In every outreach draft, mention that we can offer the lead a free health check of their SQL estate.
4. Explain that the health check will highlight where money can be saved, including SQL licensing, and identify any performance issues they may have.

Output only the HTML email body.
Do not include code fences.
Do not include a <style> block.
Use only basic HTML tags such as <b>, <i>, <br>, <p>, <ul>, <li>, and <table> where useful.

Output Format Rules:
1. Tables: Use a Markdown table for price comparisons only if price comparisons are included, with columns: Ticker | Price | Change | Target.
2. No Code Fences: Do not wrap the email content in ```html or ```markdown blocks.
3. No Naked CSS: Do not include a <style> block. Use only basic HTML tags.

If AgentMail is exposed as an OpenClaw tool rather than a CLI, the send step should change from agentmail send ... to whatever wrapper command you use to call that tool. The important safety bit is keeping approval: required before the send, so you can review the generated HTML before anything leaves. 🦞

tacit tulip
#

I have openclaw.invoke wired up, here is a working lobster file
name: t

steps:

  • id: classify_lead
    command: >-
    openclaw.invoke --tool llm-task --action json --args-json '{"prompt":"Classify this sales lead. Return JSON only with company, fit, priority, and reason. priority must be a string from 1 to 5.","thinking":"low","input":{"company":"Acme Dental","notes":"Multi-location dental practice looking for help with missed-call followup and appointment reminders."},"schema":{"type":"object","properties":{"company":{"type":"string"},"fit":{"type":"string","enum":["high","medium","low"]},"priority":{"type":"string","enum":["1","2","3","4","5"]},"reason":{"type":"string"}},"required":["company","fit","priority","reason"],"additionalProperties":false}}'

  • id: normalize_result

    command: >-
    node -e 'let s=""; process.stdin.on("data", c => s += c); process.stdin.on("end", () => { const raw = JSON.parse(s); const json = Array.isArray(raw) ? raw[0].details.json : raw.details.json; console.log(JSON.stringify({ company: json.company, fit: json.fit, priority: Number(json.priority), reason: json.reason }, null, 2)); })'
    stdin: $classify_lead.stdout

  • id: draft_outreach
    command: >-
    node -e 'let s=""; process.stdin.on("data", c => s += c); process.stdin.on("end", () => { const lead = JSON.parse(s); const draft = { subject: "Quick idea for " + lead.company, body: "Hi — noticed " + lead.company + " may benefit from faster missed-call followup and appointment reminders. Based on the lead notes, this looks like a " + lead.fit + "-fit opportunity with priority " + lead.priority + ". Worth a quick chat?", lead }; console.log(JSON.stringify(draft, null, 2)); })'
    stdin: $normalize_result.stdout

  • id: review_draft
    command: cat
    stdin: $draft_outreach.stdout
    approval: required

  • id: final_output
    command: cat
    stdin: $draft_outreach.stdout
    condition: $review_draft.approved

dire cipherBOT
#

Perfect — if openclaw.invoke is wired up, here’s the cleaner version using your pattern, with the Markdown leads file passed into llm-task, then AgentMail sending only after approval.

You’ll need to set inbox_id to the AgentMail inbox/address you send from.

name: sql_lead_drafts_agentmail

args:
  leads_file:
    default: "./leads.md"
  to:
    required: true
  inbox_id:
    required: true
  subject:
    default: "SQL estate health check lead drafts"
#

steps:
  - id: read_leads
    command: cat "{{args.leads_file}}"

  - id: build_llm_request
    command: >-
      node -e 'let leads=""; process.stdin.on("data", c => leads += c); process.stdin.on("end", () => { const req = { prompt: `You will receive a Markdown leads file. Draft one HTML email to me containing: 1. a concise summary of each lead, and 2. a tailored outreach draft for each lead. In every outreach draft, mention that we can offer the lead a free health check of their SQL estate. Explain that the health check will highlight where money can be saved, including SQL licensing, and identify any performance issues they may have. Output only structured JSON with subject and html. The html must be the email body only. Do not include markdown code fences. Do not include a <style> block. Use only basic HTML tags such as <b>, <i>, <br>, <p>, <ul>, <li>, and <table>. If any price comparison is included, use a Markdown table with columns: Ticker | Price | Change | Target.`, thinking: "low", input: { leads_markdown: leads }, schema: { type: "object", properties: { subject: { type: "string" }, html: { type: "string" } }, required: ["subject", "html"], additionalProperties: false } }; process.stdout.write(JSON.stringify(req)); })'
    stdin: $read_leads.stdout

  - id: draft_email
    command: >-
      node -e 'let s=""; process.stdin.on("data", c => s += c); process.stdin.on("end", () => { const cp = require("child_process"); const r = cp.spawnSync("openclaw.invoke", ["--tool", "llm-task", "--action", "json", "--args-json", s], { encoding: "utf8" }); if (r.stderr) process.stderr.write(r.stderr); if (r.status !== 0) process.exit(r.status); process.stdout.write(r.stdout); })'
    stdin: $build_llm_request.stdout
#
  - id: normalize_email
    command: >-
      node -e 'let s=""; process.stdin.on("data", c => s += c); process.stdin.on("end", () => { const raw = JSON.parse(s); const json = Array.isArray(raw) ? raw[0].details.json : raw.details.json; console.log(JSON.stringify({ subject: json.subject || "{{args.subject}}", html: json.html }, null, 2)); })'
    stdin: $draft_email.stdout

  - id: review_email
    command: cat
    stdin: $normalize_email.stdout
    approval: required

  - id: send_agentmail
    command: >-
      python3 -c 'import json, os, sys; from agentmail import AgentMail; email=json.load(sys.stdin); client=AgentMail(api_key=os.environ.get("AGENTMAIL_API_KEY")); client.inboxes.messages.send(inbox_id="{{args.inbox_id}}", to="{{args.to}}", subject=email.get("subject") or "{{args.subject}}", html=email["html"]); print(json.dumps({"sent": True, "to": "{{args.to}}", "subject": email.get("subject") or "{{args.subject}}"}, indent=2))'
    stdin: $normalize_email.stdout
    condition: $review_email.approved
#

Run args would look like:

{
  "leads_file": "./leads.md",
  "to": "you@example.com",
  "inbox_id": "your-inbox@agentmail.to"
}

Tiny caveat: this assumes the Python AgentMail SDK is installed in the same environment Lobster runs in, and AGENTMAIL_API_KEY is available to that process. If your AgentMail install exposes a different OpenClaw tool name/action, swap only the final send_agentmail step; the draft/review flow can stay the same.

tacit tulip
#

Agentmail is only availble in open claw so change this so that it outputs to the pipline and I can always get openclaw to send the lobster output as an email

dire cipherBOT
#

Yep — in that case remove AgentMail from the Lobster workflow entirely. Let Lobster produce a clean final output, then ask OpenClaw to email that output using AgentMail afterwards.

Here’s the adjusted .lobster file:

name: sql_lead_drafts_email_output

args:
  leads_file:
    default: "./leads.md"
  subject:
    default: "SQL estate health check lead drafts"

steps:
  - id: read_leads
    command: cat "{{args.leads_file}}"
#

  - id: build_llm_request
    command: >-
      node -e 'let leads=""; process.stdin.on("data", c => leads += c); process.stdin.on("end", () => { const req = { prompt: `You will receive a Markdown leads file. Draft one HTML email containing: 1. a concise summary of each lead, and 2. a tailored outreach draft for each lead. In every outreach draft, mention that we can offer the lead a free health check of their SQL estate. Explain that the health check will highlight where money can be saved, including SQL licensing, and identify any performance issues they may have. Output only structured JSON with subject and html. The html must be the email body only. Do not include markdown code fences. Do not include a <style> block. Use only basic HTML tags such as <b>, <i>, <br>, <p>, <ul>, <li>, and <table>. If any price comparison is included, use a Markdown table with columns: Ticker | Price | Change | Target.`, thinking: "low", input: { leads_markdown: leads }, schema: { type: "object", properties: { subject: { type: "string" }, html: { type: "string" } }, required: ["subject", "html"], additionalProperties: false } }; process.stdout.write(JSON.stringify(req)); })'
    stdin: $read_leads.stdout

  - id: draft_email
    command: >-
      node -e 'let s=""; process.stdin.on("data", c => s += c); process.stdin.on("end", () => { const cp = require("child_process"); const r = cp.spawnSync("openclaw.invoke", ["--tool", "llm-task", "--action", "json", "--args-json", s], { encoding: "utf8" }); if (r.stderr) process.stderr.write(r.stderr); if (r.status !== 0) process.exit(r.status); process.stdout.write(r.stdout); })'
    stdin: $build_llm_request.stdout

  - id: normalize_email
    command: >-
      node -e 'let s=""; process.stdin.on("data", c => s += c); process.stdin.on("end", () => { const raw = JSON.parse(s); const json = Array.isArray(raw) ? raw[0].details.json : raw.details.json; console.log(JSON.stringify({ subject: json.subject || "{{args.subject}}", contentT
#
    stdin: $draft_email.stdout

  - id: review_email
    command: cat
    stdin: $normalize_email.stdout
    approval: required

  - id: final_output
    command: cat
    stdin: $normalize_email.stdout
    condition: $review_email.approved

Then after it runs, tell OpenClaw something like:

Send the Lobster final_output as an HTML email to me using AgentMail. Use subject as the subject and html as the email body.

#

The final output will be JSON shaped like:

{
  "subject": "SQL estate health check lead drafts",
  "contentType": "text/html",
  "html": "<p>...</p>"
}

That keeps Lobster deterministic and reviewable, while letting OpenClaw handle the AgentMail-only part outside the pipeline.

tacit tulip
#

I ran that but had to abort it, here is the output

[12/05/2026 13:06] DBA Lead Scout: The error is on line 25 — the line got corrupted. It reads yamlype instead of contentType. Let me fix it:
[12/05/2026 13:06] DBA Lead Scout: The line got split and corrupted — contentType became contentT + newline + yamlype. Let me fix it:
[12/05/2026 13:06] DBA Lead Scout: Fixed. Now re-running the pipeline:
[12/05/2026 13:06] DBA Lead Scout: Progress — the YAML is now valid. The next error is that {{args.leads_file}} isn't being resolved by the lobster runtime. Let me check the leads file exists and fix the template syntax:
[12/05/2026 13:06] DBA Lead Scout: The leads file exists. The issue is that {{args.leads_file}} isn't being resolved by the lobster runtime. Let me check the lobster skill for the correct template syntax:
[12/05/2026 13:07] DBA Lead Scout: The lobster skill shows template variables use ${var} syntax, not {{args.var}}. Let me check the existing lobster files to see what syntax they use:
[12/05/2026 13:07] DBA Lead Scout: The lobster tool's pipeline argument expects a pipeline name, not a file path. The {{args.leads_file}} template syntax also isn't being resolved. Let me try passing the leads file directly as an argument:
[12/05/2026 13:07] DBA Lead Scout: The lobster tool doesn't resolve {{args.var}} — it uses ${var} syntax. Let me fix the pipeline:
[12/05/2026 13:07] DBA Lead Scout: The ${var} syntax is causing YAML parse issues. Let me hardcode the paths since they're known:
[12/05/2026 13:07] DBA Lead Scout: The complex node -e one-liners with backticks and nested quotes are breaking the YAML parser. Let me rewrite the pipeline using separate script files:

dire cipherBOT
#

Yep, that failure is on me — I gave you two risky things at once: unsupported {{args...}} interpolation and horrible YAML-hostile one-liners. 🦞

Use a boring Lobster file + small script files. Much less cursed.

sql-lead-drafts.lobster:

name: sql_lead_drafts_email_output

steps:
  - id: read_leads
    command: cat ./leads.md

  - id: build_llm_request
    command: node ./lobster-scripts/build-llm-request.js
    stdin: $read_leads.stdout
#

  - id: draft_email
    command: node ./lobster-scripts/invoke-llm-task.js
    stdin: $build_llm_request.stdout

  - id: normalize_email
    command: node ./lobster-scripts/normalize-email.js
    stdin: $draft_email.stdout

  - id: review_email
    command: cat
    stdin: $normalize_email.stdout
    approval: required

  - id: final_output
#
    command: cat
    stdin: $normalize_email.stdout
    condition: $review_email.approved

Create lobster-scripts/build-llm-request.js:

let leads = "";
process.stdin.on("data", c => leads += c);
process.stdin.on("end", () => {
  const req = {
    prompt: [
      "You will receive a Markdown leads file.",
      "Draft one HTML email containing:",
#
      "1. A concise summary of each lead.",
      "2. A tailored outreach draft for each lead.",
      "In every outreach draft, mention that we can offer the lead a free health check of their SQL estate.",
      "Explain that the health check will highlight where money can be saved, including SQL licensing, and identify any performance issues they may have.",
      "Output only structured JSON with subject and html.",
      "The html must be the email body only.",
      "Do not include markdown code fences.",
      "Do not include a <style> block.",
      "Use only basic HTML tags such as <b>, <i>, <br>, <p>, <ul>, <li>, and <table>.",
      "If any price comparison is included, use a Markdown table with columns: Ticker | Price | Change | Target."
    ].join("
"),
    thinking: "low",
    input: { leads_markdown: leads },
    schema: {
#
      type: "object",
      properties: {
        subject: { type: "string" },
        html: { type: "string" }
      },
      required: ["subject", "html"],
      additionalProperties: false
    }
  };

  process.stdout.write(JSON.stringify(req));
});

Create lobster-scripts/invoke-llm-task.js:

#
const cp = require("child_process");

let argsJson = "";
process.stdin.on("data", c => argsJson += c);
process.stdin.on("end", () => {
  const r = cp.spawnSync(
    "openclaw.invoke",
    ["--tool", "llm-task", "--action", "json", "--args-json", argsJson],
    { encoding: "utf8" }
  );

  if (r.stderr) process.stderr.write(r.stderr);
  if (r.status !== 0) process.exit(r.status || 1);
  process.stdout.write(r.stdout);
});
#

Create lobster-scripts/normalize-email.js:

let s = "";
process.stdin.on("data", c => s += c);
process.stdin.on("end", () => {
  const raw = JSON.parse(s);
  const json = Array.isArray(raw) ? raw[0].details.json : raw.details.json;

  console.log(JSON.stringify({
    subject: json.subject || "SQL estate health check lead drafts",
    contentType: "text/html",
    html: json.html
  }, null, 2));
});
#

Then put leads.md next to the .lobster file and run it from that directory.

Important correction: don’t use {{args.leads_file}} here. Your runtime clearly isn’t resolving that inside workflow commands, so hardcoding ./leads.md is the safest path. Once final_output is approved, OpenClaw can take that JSON and send html as an AgentMail HTML email.

tacit tulip
#

The lobster tool failed with this exact error:

workflow command failed (1): /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/build-llm-request.js:18
].join("")
^

SyntaxError: Invalid or unexpected token

dire cipherBOT
#

That’s the JS file getting mangled at the newline escape — likely " " became an actual newline inside the quoted string. Easiest fix: avoid escape sequences entirely.

Replace lobster-scripts/build-llm-request.js with this version:

let leads = "";
process.stdin.on("data", c => leads += c);
process.stdin.on("end", () => {
  const lines = [
    "You will receive a Markdown leads file.",
    "Draft one HTML email containing:",
    "1. A concise summary of each lead.",
    "2. A tailored outreach draft for each lead.",
    "In every outreach draft, mention that we can offer the lead a free health check of their SQL estate.",
    "Explain that the health check will highlight where money can be saved, including SQL licensing, and identify any performance issues they may have.",
#
    "Output only structured JSON with subject and html.",
    "The html must be the email body only.",
    "Do not include markdown code fences.",
    "Do not include a style block.",
    "Use only basic HTML tags such as b, i, br, p, ul, li, and table.",
    "If any price comparison is included, use a Markdown table with columns: Ticker | Price | Change | Target."
  ];

  const req = {
    prompt: lines.join(String.fromCharCode(10)),
    thinking: "low",
    input: { leads_markdown: leads },
    schema: {
      type: "object",
      properties: {
#
        subject: { type: "string" },
        html: { type: "string" }
      },
      required: ["subject", "html"],
      additionalProperties: false
    }
  };

  process.stdout.write(JSON.stringify(req));
});

Then quick sanity check before rerunning Lobster:

#
node ./lobster-scripts/build-llm-request.js < ./leads.md > /tmp/request.json
node -e 'JSON.parse(require("fs").readFileSync("/tmp/request.json", "utf8")); console.log("ok")'

If that prints ok, rerun the Lobster pipeline. The String.fromCharCode(10) bit avoids Discord/copy-paste turning into broken JavaScript.

tacit tulip
#

sanity check works. But running it failed with this

The lobster tool failed with this exact error:

workflow command failed (2): Parse error: Unclosed quote

This is a parse error in the pipeline file /home/admin/.openclaw/workspace-dba-scout/sql-leads-draft.lobster — likely a stray or missing quote character somewhere in the file.

dire cipherBOT
#

Nice, sanity check passing means the JS is fine. This error is probably Lobster’s command parser choking on quoted shell text in the .lobster file — not the JS file.

Replace the workflow with a no-quotes version:

name: sql_lead_drafts_email_output

steps:
  - id: read_leads
    command: cat leads.md

  - id: build_llm_request
    command: node lobster-scripts/build-llm-request.js
    stdin: $read_leads.stdout

  - id: draft_email
#
    command: node lobster-scripts/invoke-llm-task.js
    stdin: $build_llm_request.stdout

  - id: normalize_email
    command: node lobster-scripts/normalize-email.js
    stdin: $draft_email.stdout

  - id: review_email
    command: cat
    stdin: $normalize_email.stdout
    approval: required

  - id: final_output
    command: cat
    stdin: $normalize_email.stdout
#
    condition: $review_email.approved

Key changes:

  • cat ./leads.md → cat leads.md
  • no quotes anywhere in command: values
  • no wrapped >- commands

Also check the file with:

nl -ba sql-leads-draft.lobster

If you see any smart quotes like “ / ”, delete and retype them — but the version above avoids quotes completely, so it should sidestep the parser issue.

tacit tulip
#

This is the contents that needs fixing
name: sql_lead_drafts_email_output

steps:

  • id: read_leads
    command: cat /home/admin/.openclaw/workspace-dba-scout/sql_leads.md

  • id: build_llm_request
    command: node /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/build-llm-request.js
    stdin: $read_leads.stdout

  • id: draft_email
    command: node /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/invoke-llm-task.js
    stdin: $build_llm_request.stdout

  • id: normalize_email
    command: node /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/normalize-email.js
    stdin: $draft_email.stdout

  • id: review_email
    command: cat
    stdin: $normalize_email.stdout
    approval: required

  • id: final_output
    command: cat
    stdin: $normalize_email.stdout
    condition: $review_email.approved

dire cipherBOT
#

The structure is basically right, but use this exact cleaned version — consistent 2-space YAML indentation, no quotes, no wrapped commands:

name: sql_lead_drafts_email_output

steps:
  - id: read_leads
    command: cat /home/admin/.openclaw/workspace-dba-scout/sql_leads.md

  - id: build_llm_request
    command: node /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/build-llm-request.js
    stdin: $read_leads.stdout

  - id: draft_email
    command: node /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/invoke-llm-task.js
    stdin: $build_llm_request.stdout
#

  - id: normalize_email
    command: node /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/normalize-email.js
    stdin: $draft_email.stdout

  - id: review_email
    command: cat
    stdin: $normalize_email.stdout
    approval: required

  - id: final_output
    command: cat
    stdin: $normalize_email.stdout
    condition: $review_email.approved
#

If that exact file still gives Parse error: Unclosed quote, then the file probably has an invisible/smart quote character from copy-paste. Easiest reset:

cat > /home/admin/.openclaw/workspace-dba-scout/sql-leads-draft.lobster <<'EOF'
name: sql_lead_drafts_email_output

steps:
  - id: read_leads
    command: cat /home/admin/.openclaw/workspace-dba-scout/sql_leads.md

  - id: build_llm_request
    command: node /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/build-llm-request.js
    stdin: $read_leads.stdout

  - id: draft_email
    command: node /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/invoke-llm-task.js
#
    stdin: $build_llm_request.stdout

  - id: normalize_email
    command: node /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/normalize-email.js
    stdin: $draft_email.stdout

  - id: review_email
    command: cat
    stdin: $normalize_email.stdout
    approval: required

  - id: final_output
    command: cat
    stdin: $normalize_email.stdout
    condition: $review_email.approved
#
EOF

That heredoc should strip out whatever weird copy-paste character is causing the quote parser to panic.

tacit tulip
#

I get the same error, could it be that the issue is the contenst of the sql_leads.md

dire cipherBOT
#

Yes — at this point it could be sql_leads.md, but not because Markdown is invalid. More likely one of the leads contains an apostrophe/quote/backtick that gets embedded into the huge --args-json value, and openclaw.invoke or the bridge parser is choking with Unclosed quote.

Your .lobster file has no quotes left, so I’d stop passing the whole leads file through a CLI arg. Replace lobster-scripts/invoke-llm-task.js with a direct /tools/invoke call so the request goes over JSON HTTP instead of command-line quoting:

let s = "";
process.stdin.on("data", c => s += c);
process.stdin.on("end", async () => {
  const args = JSON.parse(s);

  const port = process.env.OPENCLAW_GATEWAY_PORT || "18789";
  const baseUrl = process.env.OPENCLAW_GATEWAY_URL || `http://127.0.0.1:${port}`;
  const token = process.env.OPENCLAW_GATEWAY_TOKEN || process.env.SHARED_GATEWAY_TOKEN || "";

  const headers = { "content-type": "application/json" };
  if (token) headers.authorization = `Bearer ${token}`;
#

  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/tools/invoke`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      tool: "llm-task",
      action: "json",
      args
    })
  });

  const text = await res.text();
  if (!res.ok) {
    console.error(text);
    process.exit(1);
#
  }

  process.stdout.write(text);
});

Then run this sanity check:

node lobster-scripts/build-llm-request.js < sql_leads.md > /tmp/request.json
node lobster-scripts/invoke-llm-task.js < /tmp/request.json > /tmp/llm-result.json
node lobster-scripts/normalize-email.js < /tmp/llm-result.json

If the second command fails with auth, set the same gateway token your openclaw.invoke uses in the shell running Lobster, usually:

#
export OPENCLAW_GATEWAY_TOKEN='your-token-here'

If this direct HTTP version works, rerun the Lobster pipeline unchanged. This bypasses the quote parser entirely, so lead contents like Bob's SQL Server won’t explode the pipeline.

tacit tulip
#

i did have to export the token but then I got this message

{"ok":false,"error":{"type":"tool_error","message":"tool execution failed"}}

dire cipherBOT
#

Could be, yes — but that response is the gateway hiding the real llm-task exception behind a generic /tools/invoke 500. We need isolate whether it’s the lead content or the llm-task config/model.

First, run a tiny llm-task call with the same token:

cat >/tmp/tiny-llm-request.json <<'EOF'
{
  "prompt": "Return JSON with ok true.",
  "thinking": "low",
  "input": { "test": "hello" },
  "schema": {
    "type": "object",
    "properties": { "ok": { "type": "boolean" } },
    "required": ["ok"],
    "additionalProperties": false
  }
#
}
EOF

node lobster-scripts/invoke-llm-task.js < /tmp/tiny-llm-request.json

If that fails too, it’s not sql_leads.md — it’s llm-task model/auth/config. Check logs:

openclaw logs --tail 100 | grep -i "tools-invoke\|llm-task\|tool execution failed"

If the tiny one works, then it’s probably the leads content size or the generated output is too large / timing out. Update build-llm-request.js to add bigger limits:

#
maxTokens: 6000,
timeoutMs: 120000,

Put those next to thinking: "low", like:

thinking: "low",
maxTokens: 6000,
timeoutMs: 120000,
input: { leads_markdown: leads },

Also check size:

#
wc -c sql_leads.md /tmp/request.json

If sql_leads.md is huge, split it into smaller lead batches. The /tools/invoke body limit is around 2MB, and model context/output limits can bite well before that.

One more fix: once /tools/invoke succeeds, its output is wrapped as { ok: true, result: ... }, so normalize-email.js should handle that too. Replace the JSON extraction line with:

const json = raw.result?.details?.json || raw.details?.json || (Array.isArray(raw) ? raw[0].details.json : null);

But first run the tiny request — that’ll tell us which branch we’re in.

tacit tulip
#

ah I can see this in openclaw log
{"ok":false,"error":{"type":"tool_error","message":"tool execution failed"}}

dire cipherBOT
#

That line is the HTTP response body, not the useful underlying error. OpenClaw logs should have a nearby line just before/after it that starts something like:

tools-invoke: tool execution failed: ...

Run this and paste the matching lines around it:

openclaw logs --tail 300 | grep -i -C 5 "tools-invoke\|llm-task\|provider/model\|invalid json\|empty output\|timed out\|Model not allowed"

Most likely causes from this point:

  • llm-task can’t resolve a provider/model
  • thinking: "low" isn’t supported by the selected model
  • model/auth failure inside the embedded LLM run
  • timeout / output too large
#
  • model returned non-JSON

Quick isolation: temporarily remove this line from build-llm-request.js:

thinking: "low",

Then try the tiny request again. If it works after removing thinking, the selected model doesn’t support that thinking level and the pipeline itself is fine.

tacit tulip
#

I mean this

13:47:29
error
diagnostic
lane task error: lane=main durationMs=31758 error="FailoverError: LLM request timed out."
13:47:29
error
diagnostic
lane task error: lane=session:agent:main:explicit:llm-task-1778590017778 durationMs=31764 error="FailoverError: LLM request timed out."

dire cipherBOT
#

Yep — that’s the real issue now. Good catch. It’s no longer the Markdown/quote problem; llm-task is hitting its default ~30s timeout.

Because the log says durationMs=31758, your request is probably still using the default llm-task timeout. Add timeoutMs and probably maxTokens to the JSON request in build-llm-request.js:

thinking: "low",
timeoutMs: 180000,
maxTokens: 8000,
input: { leads_markdown: leads },

Then verify the generated request actually contains it:

node lobster-scripts/build-llm-request.js < sql_leads.md > /tmp/request.json
node -e 'const j=require("./tmp/request.json")' 2>/dev/null || node -e 'const j=JSON.parse(require("fs").readFileSync("/tmp/request.json","utf8")); console.log({timeoutMs:j.timeoutMs,maxTokens:j.maxTokens,thinking:j.thinking})'
#

You want to see something like:

{ timeoutMs: 180000, maxTokens: 8000, thinking: 'low' }

If it still times out after ~180s, split the leads into smaller batches. Drafting summaries + tailored HTML for many leads is likely too much for one llm-task call.

A practical prompt tweak that helps a lot: tell it to keep each lead short, e.g. add this line to the prompt array:

"Keep each lead summary under 80 words and each outreach draft under 150 words."

Also: when you run the Lobster tool itself from OpenClaw, give the Lobster call a bigger timeoutMs too, otherwise the outer workflow may time out even if llm-task is allowed to run longer.

tacit tulip
#

Can you give me the updated build-llm-request.js

dire cipherBOT
#

Yep — replace lobster-scripts/build-llm-request.js with this:

let leads = "";

process.stdin.on("data", c => {
  leads += c;
});

process.stdin.on("end", () => {
  const lines = [
    "You will receive a Markdown leads file.",
    "Draft one HTML email containing:",
    "1. A concise summary of each lead.",
    "2. A tailored outreach draft for each lead.",
    "In every outreach draft, mention that we can offer the lead a free health check of their SQL estate.",
#
    "Explain that the health check will highlight where money can be saved, including SQL licensing, and identify any performance issues they may have.",
    "Keep each lead summary under 80 words.",
    "Keep each outreach draft under 150 words.",
    "Output only structured JSON with subject and html.",
    "The html must be the email body only.",
    "Do not include markdown code fences.",
    "Do not include a style block.",
    "Use only basic HTML tags such as b, i, br, p, ul, li, and table.",
    "If any price comparison is included, use a Markdown table with columns: Ticker | Price | Change | Target."
  ];

  const req = {
    prompt: lines.join(String.fromCharCode(10)),
    thinking: "low",
    timeoutMs: 180000,
#
    maxTokens: 8000,
    input: {
      leads_markdown: leads
    },
    schema: {
      type: "object",
      properties: {
        subject: { type: "string" },
        html: { type: "string" }
      },
      required: ["subject", "html"],
      additionalProperties: false
    }
  };
#
  process.stdout.write(JSON.stringify(req));
});

Then sanity check it contains the new timeout:

node lobster-scripts/build-llm-request.js < sql_leads.md > /tmp/request.json
node -e 'const j=JSON.parse(require("fs").readFileSync("/tmp/request.json","utf8")); console.log({timeoutMs:j.timeoutMs,maxTokens:j.maxTokens,thinking:j.thinking})'

Expected:

{ timeoutMs: 180000, maxTokens: 8000, thinking: 'low' }
tacit tulip
#

do i have to update invoke-llm-task.js too?

dire cipherBOT
#

Probably not, if your invoke-llm-task.js is the direct /tools/invoke version I gave you. It just forwards whatever JSON build-llm-request.js produces, so the new timeoutMs: 180000 and maxTokens: 8000 will be passed through automatically.

Quick check:

node lobster-scripts/build-llm-request.js < sql_leads.md > /tmp/request.json
node -e 'const j=JSON.parse(require("fs").readFileSync("/tmp/request.json","utf8")); console.log(j.timeoutMs, j.maxTokens)'
node lobster-scripts/invoke-llm-task.js < /tmp/request.json > /tmp/llm-result.json

If that works, no update needed.

Two things you may still need:

  1. Run the Lobster tool itself with a bigger outer timeout, because the workflow now may run longer than 30s.

  2. Make sure normalize-email.js handles the /tools/invoke wrapper. The extraction line should be:

#
const json = raw.result?.details?.json || raw.details?.json || (Array.isArray(raw) ? raw[0].details.json : null);

So: invoke-llm-task.js no, normalize-email.js maybe yes.

tacit tulip
#

It got further, but now I have this error in the openclaw log
14:16:19
debug
tools: lobster failed stack:
Error: workflow command failed (1): /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/normalize-email.js:5
const json = Array.isArray(raw) ? raw[0].details.json : raw.details.json;
^

TypeError: Cannot read properties of undefined (reading 'json')
at Socket.<anonymous> (/home/admin/.openclaw/workspace-dba-scout/lobster-scripts/normalize-email.js:5:71)
at Socket.emit (node:events:531:35)
at endReadableNT (node:internal/streams/readable:1698:12)
at process.processTicksAndRejections (node:internal/process/task_queues:89:21)

Node.js v22.22.2
at throwOnErrorEnvelope (/home/admin/.openclaw/npm/node_modules/@openclaw/lobster/dist/index.js:175:9)
at /home/admin/.openclaw/npm/node_modules/@openclaw/lobster/dist/index.js:276:20
at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
14:16:19
error
[tools] lobster failed: workflow command failed (1): /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/normalize-email.js:5
const json = Array.isArray(raw) ? raw[0].details.json : raw.details.json;
^

TypeError: Cannot read properties of undefined (reading 'json')
at Socket.<anonymous> (/home/admin/.openclaw/workspace-dba-scout/lobster-scripts/normalize-email.js:5:71)
at Socket.emit (node:events:531:35)
at endReadableNT (node:internal/streams/readable:1698:12)
at process.processTicksAndRejections (node:internal/process/task_queues:89:21)

Node.js v22.22.2 raw_params={"action":"run","pipeline":"/home/admin/.openclaw/workspace-dba-scout/sql-leads-draft.lobster","timeoutMs":600000,"maxStdoutBytes":512000}

dire cipherBOT
#

Yep — that’s exactly the wrapper mismatch I mentioned. Since you switched to direct /tools/invoke, the result shape is now likely:

{
  "ok": true,
  "result": {
    "details": {
      "json": { ... }
    }
  }
}

Replace lobster-scripts/normalize-email.js with this more defensive version:

#
let s = "";

process.stdin.on("data", c => {
  s += c;
});

process.stdin.on("end", () => {
  const raw = JSON.parse(s);

  const json =
    raw?.result?.details?.json ||
    raw?.details?.json ||
    (Array.isArray(raw) ? raw[0]?.details?.json : null);

  if (!json) {
#
    console.error("Could not find details.json in llm-task output.");
    console.error(JSON.stringify(raw, null, 2).slice(0, 4000));
    process.exit(1);
  }

  console.log(JSON.stringify({
    subject: json.subject || "SQL estate health check lead drafts",
    contentType: "text/html",
    html: json.html
  }, null, 2));
});

Then test just the last part:

#
node lobster-scripts/normalize-email.js < /tmp/llm-result.json

If that prints the { subject, contentType, html } JSON, rerun the Lobster pipeline. If it still fails, the script will now print the first 4000 chars of the actual shape, which will tell us what wrapper your /tools/invoke is returning.

tacit tulip
#

CAn you remove the approval from the workflow

dire cipherBOT
#

Yep — since this workflow only produces output and doesn’t send the email itself, it’s fine to remove the approval gate.

Use this updated .lobster file:

name: sql_lead_drafts_email_output

steps:
  - id: read_leads
    command: cat /home/admin/.openclaw/workspace-dba-scout/sql_leads.md

  - id: build_llm_request
    command: node /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/build-llm-request.js
    stdin: $read_leads.stdout

  - id: draft_email
#
    command: node /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/invoke-llm-task.js
    stdin: $build_llm_request.stdout

  - id: normalize_email
    command: node /home/admin/.openclaw/workspace-dba-scout/lobster-scripts/normalize-email.js
    stdin: $draft_email.stdout

  - id: final_output
    command: cat
    stdin: $normalize_email.stdout

That removes both:

#
approval: required

and the approval condition:

condition: $review_email.approved

So the pipeline will now run straight through and output the final { subject, contentType, html } JSON.

tacit tulip
#

I'm using this prompt to execute it, rather than hard code a timeout in build-llm-request.js , can it somehow pickup the timeout from the one specified in the prompt?

all the OpenClaw tool named lobster directly.

Use this exact tool payload:
{
"action": "run",
"pipeline": "/home/admin/.openclaw/workspace-dba-scout/sql-leads-draft.lobster",
"timeoutMs": 600000,
"maxStdoutBytes": 512000
}

If this fails, tell me the exact lobster tool error. Do not try to work around it with shell or the standalone Lobster CLI.

Email me the output using agentmail

dire cipherBOT
#

Short answer: not from that timeoutMs, no.

That timeoutMs: 600000 belongs to the outer Lobster tool call. It controls how long OpenClaw lets the whole workflow run. It is not automatically passed into your workflow steps, and llm-task has its own separate timeoutMs argument.

So you have three realistic options:

  1. Keep timeoutMs in build-llm-request.js — simplest.
  2. Read it from an env var — flexible, but you must export it before running.
  3. Pass a separate value via workflow args — possible in theory, but your runtime already showed command interpolation is flaky, so I’d avoid it here.

I’d use env var fallback. Update build-llm-request.js like this:

let leads = "";

process.stdin.on("data", c => {
#
  leads += c;
});

process.stdin.on("end", () => {
  const llmTimeoutMs = Number(process.env.LLM_TASK_TIMEOUT_MS || "180000");
  const maxTokens = Number(process.env.LLM_TASK_MAX_TOKENS || "8000");

  const lines = [
    "You will receive a Markdown leads file.",
    "Draft one HTML email containing:",
    "1. A concise summary of each lead.",
    "2. A tailored outreach draft for each lead.",
    "In every outreach draft, mention that we can offer the lead a free health check of their SQL estate.",
    "Explain that the health check will highlight where money can be saved, including SQL licensing, and identify any performance issues they may have.",
    "Keep each lead summary under 80 words.",
#
    "Keep each outreach draft under 150 words.",
    "Output only structured JSON with subject and html.",
    "The html must be the email body only.",
    "Do not include markdown code fences.",
    "Do not include a style block.",
    "Use only basic HTML tags such as b, i, br, p, ul, li, and table.",
    "If any price comparison is included, use a Markdown table with columns: Ticker | Price | Change | Target."
  ];

  const req = {
    prompt: lines.join(String.fromCharCode(10)),
    thinking: "low",
    timeoutMs: llmTimeoutMs,
    maxTokens,
    input: {
#
      leads_markdown: leads
    },
    schema: {
      type: "object",
      properties: {
        subject: { type: "string" },
        html: { type: "string" }
      },
      required: ["subject", "html"],
      additionalProperties: false
    }
  };

  process.stdout.write(JSON.stringify(req));
});
#

Then set:

export LLM_TASK_TIMEOUT_MS=600000
export LLM_TASK_MAX_TOKENS=8000

Your OpenClaw prompt should still keep the outer Lobster timeout too:

{
  "action": "run",
  "pipeline": "/home/admin/.openclaw/workspace-dba-scout/sql-leads-draft.lobster",
  "timeoutMs": 600000,
  "maxStdoutBytes": 512000
}
#

Think of it as two timers: outer workflow timer + inner LLM timer. Both need enough time.

tacit tulip
#

ok, everything is working

dire cipherBOT
#

Brilliant — glad we got it untangled 🎉

If this fixed it, please right-click the solution message → Apps → ✅ Mark Solution so Answer Overflow can index it for the next person who hits Lobster + llm-task timeout/quoting weirdness.