How to Create NetSuite Sales Orders from Emailed PO PDFs

Automatically turn purchase orders received by email into NetSuite sales orders, with the PO PDF parsed by AI in a single workflow run.

What This Integration Does

Many B2B sales teams receive purchase orders as PDF attachments to email. Re-keying those POs into NetSuite is slow, error-prone, and exactly the kind of repetitive work that delays fulfilment. This workflow watches a dedicated inbox (e.g. orders@yourcompany.com), extracts the PO PDF on every new message, uses AI to read the line items and header fields out of the document, and creates the matching sales order in NetSuite - end-to-end, with no human in the loop for clean POs.

The Knowledge node does the heavy lifting in Transient mode: the PDF is embedded into a single-run collection, queried with a structured response schema to extract a typed PO object, and the collection is automatically cleaned up when the workflow finishes. Nothing about that one customer's PO is persisted in your knowledge base. A Condition node routes unrecognised PDFs (the AI couldn't parse a confident PO out of them) to a human-review path so nothing is silently dropped.

Prerequisites

  • A Gmail or Outlook connection on the inbox that receives POs. Use a dedicated alias (e.g. orders@) so the trigger doesn't fire on every email.
  • A NetSuite connection with REST + SuiteQL access and permission to create Sales Order records.
  • Mapping data: a way to resolve PO customer names to NetSuite customer internal IDs, and PO SKUs to NetSuite item internal IDs (a small MySQL or MongoDB lookup table, or SuiteQL queries).
  • Optionally a Slack connection for failure and human-review alerts.

Step 1: Email Trigger on the PO Inbox

Drop a Trigger node and set its type to Email, pointing at your Gmail or Outlook connection. Spojit fires once per new message with the sender, subject, body, and any attachments exposed on the trigger output. Add a filter (e.g. subject contains PO or sender domain matches your customer domains) so unrelated mail doesn't trip the workflow.

Step 2: Extract the PDF Attachment

Add a Condition node that checks {{ trigger.attachments.length > 0 }} and that the first attachment's content type is application/pdf. On the false branch, route to a slack send-message call so a human can take a look. On the true branch, continue.

The PDF bytes are available on the trigger output as {{ trigger.attachments[0].content }} (Base64-encoded) and the file name as {{ trigger.attachments[0].name }}.

Step 3: Embed the PDF into a Transient Knowledge Collection

Add a Knowledge node and configure:

  • Mode: Embed
  • Collection: Transient - a single-run collection that's automatically cleaned up at the end of the workflow.
  • Document Type: PDF
  • Document Input: {{ trigger.attachments[0].content }}

Transient collections don't require a file name or embedding model selection - they're designed for exactly this use case, where you embed, query, and discard the same document inside one workflow run.

Step 4: Query the Transient Collection with a Response Schema

Add a second Knowledge node and configure:

  • Mode: Query
  • Collection: Transient - matches the collection from Step 3 within the same run.
  • Prompt: Extract the purchase order header (PO number, customer name, billing address, requested ship date, currency) and every line item (SKU, description, quantity, unit price).
  • Response Schema: define the expected JSON shape so the result is typed and downstream nodes can rely on it.
{
  "type": "object",
  "properties": {
    "poNumber":          { "type": "string" },
    "customerName":      { "type": "string" },
    "billingAddress":    { "type": "string" },
    "requestedShipDate": { "type": ["string", "null"] },
    "currency":          { "type": "string" },
    "confidence":        { "type": "string", "enum": ["high", "medium", "low"] },
    "lineItems": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "sku":         { "type": "string" },
          "description": { "type": "string" },
          "quantity":    { "type": "number" },
          "unitPrice":   { "type": "number" }
        },
        "required": ["sku", "quantity"]
      }
    }
  },
  "required": ["poNumber", "customerName", "lineItems", "confidence"]
}

The structured output gives you a typed po object the rest of the workflow can work with.

Step 5: Resolve Customer and SKUs in NetSuite

Add a Connector node calling netsuite run-suiteql to look up the customer by name:

SELECT id, entityid, companyname FROM customer
WHERE LOWER(companyname) = LOWER('{{ po.customerName }}') AND isinactive = 'F'

If the customer doesn't exist, route to a human-review branch via a Condition node - automatically creating customers from emailed POs is risky. For SKUs, run a second SuiteQL or look them up in a mapping table; either way, every line item needs a NetSuite item internal ID before the sales order can be created.

Step 6: Create the Sales Order in NetSuite

Add a Connector node calling netsuite upsert-record:

  • Record type: salesOrder
  • externalid: email-po-{{ po.poNumber }} - this makes retries idempotent so a webhook replay doesn't create duplicates.
  • entity: the customer internal ID from Step 5.
  • tranDate: {{ trigger.receivedAt }}
  • otherrefnum: {{ po.poNumber }} so finance can search NetSuite by the customer's PO number.
  • memo: From emailed PO {{ trigger.attachments[0].name }} - confidence: {{ po.confidence }}
  • item sublist: one entry per line item, with the resolved NetSuite item ID, quantity, and unit price.

On low-confidence parses (po.confidence == "low"), route to a Slack channel for human review before letting the sales order go live - use a Condition node before the upsert and a Human approval node on the slow path.

Tips

  • Always use Transient for single-PO processing. A persistent collection would leak every customer's PO into the same searchable bucket - a privacy and noise problem.
  • Pin the response schema confidence levels. The AI is much more reliable when it has to commit to high / medium / low than when you ask "are you sure?".
  • Cache SKU lookups. Run a daily SuiteQL into a MySQL or MongoDB cache and look SKUs up there - it's 10-100x faster than a per-line-item SuiteQL.
  • Reply to the sender with the NetSuite number. A short auto-reply with the sales order number closes the loop with the customer.

Common Pitfalls

  • Scanned PDFs without OCR. If the PO is a scan of a paper document, the Knowledge node can't read the text. Detect this in Step 2 with a quick text-extract check and route to a human-review path.
  • Multiple POs per email. Some customers attach several POs in one message. Loop over trigger.attachments filtered on application/pdf instead of indexing [0].
  • Customer name variations. "Acme Corp", "ACME Corporation", "Acme Co." can all be the same customer. Use SuiteQL with LIKE matching and route ambiguous matches to human review.
  • SKU drift. Customers sometimes send their internal SKU rather than your catalog SKU. Maintain a per-customer SKU alias map for the high-volume accounts.
  • Replays creating duplicate orders. Always set externalid based on the PO number so retries are idempotent.

Testing

Send a real (small) PO PDF to the trigger inbox from your own email and watch the execution log. Verify (a) the AI extracted the right fields, (b) the SuiteQL customer lookup found the right account, and (c) the resulting NetSuite sales order has the right line items and references the source PO number. Then turn the workflow on for production traffic.

Learn More

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.