-
Notifications
You must be signed in to change notification settings - Fork 5.7k
[Components] emboss - new app actions #21152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
37468ba
991a610
94f0cac
659cf4b
ee6bb48
8b4714e
db256cd
2a2ad48
548d062
d4c03d0
e262f75
dab8c7a
2e9e403
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # Overview | ||
|
|
||
| [Emboss](https://getemboss.ai) turns flat PDFs into fillable forms and fills | ||
| them with AI. The Emboss API detects the fields on any flat PDF — text | ||
| fields, checkboxes, signatures, tables — and can pre-fill them from context | ||
| documents you provide. These components wrap the three core operations: | ||
| create a fillable form, fill a PDF from context in one step, and fill a form | ||
| you already created. | ||
|
|
||
| # Example Use Cases | ||
|
|
||
| - **Intake automation** — a client uploads a government or insurance PDF; | ||
| the workflow fills it from your CRM record and files the completed copy. | ||
| - **Document generation at scale** — fill the same permit or application | ||
| form for hundreds of records from a spreadsheet or database. | ||
| - **Email-to-filled-form** — trigger on an inbound attachment, fill it from | ||
| the email body as context, and send the completed PDF back. | ||
|
|
||
| # Getting Started | ||
|
|
||
| 1. Create an Emboss account at [getemboss.ai](https://getemboss.ai) and | ||
| generate an API key from the dashboard. | ||
| 2. Connect your Emboss account in Pipedream by pasting the API key. | ||
| 3. Add one of the Emboss actions to your workflow. Form processing is | ||
| asynchronous — the action polls until the PDF is ready, then returns the | ||
| file path. | ||
|
|
||
| # Troubleshooting | ||
|
|
||
| - **"not a readable PDF"** — the File/URL you mapped must point at the PDF | ||
| bytes, not a filename string. Map the upstream step's file URL or `/tmp` | ||
| path. | ||
| - **Job failed with a detail message** — the Emboss API reports per-job | ||
| errors verbatim; the action surfaces them as the step error. | ||
| - **Long-running forms** — large PDFs can take a few minutes; the action | ||
| re-checks every 5 seconds for up to ~12 minutes, then errors so the workflow | ||
| never proceeds silently. | ||
| - **Step times out while polling** — raise the workflow's execution timeout | ||
| (Settings → Execution Controls) to at least 750 seconds; form processing takes | ||
| minutes and the default timeout can cut the poll short. | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import FormData from "form-data"; | ||
| import { ConfigurationError } from "@pipedream/platform"; | ||
| import emboss from "../../emboss.app.mjs"; | ||
| import { | ||
| resolveFileRef, writePdf, errorDetail, | ||
| } from "../../common/utils.mjs"; | ||
|
|
||
| const POLL_DELAY_MS = 5000; | ||
| const MAX_RETRIES = 144; | ||
|
|
||
| export default { | ||
| key: "emboss-create-fillable-form", | ||
| name: "Create Fillable Form", | ||
| description: "Turn a flat (non-fillable) PDF into a fillable form. Emboss detects text fields, checkboxes, signatures, and tables, and returns a fillable PDF plus a form ID. Use **Fill Existing Form** to fill it later, or **Fill PDF From Context** to detect and fill in one step. Polls up to ~12 minutes for large documents. [See the documentation](https://getemboss.ai/docs)", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The description doc link is a generic 'See the documentation' (redirects to quickstart) rather than deep-linking to the relevant reference page. House rules require deep-linked doc URLs. Applies to all three actions (fill-existing-form and fill-from-pdf-context should point to /docs/reference/fill-with-context; create-fillable-form to /docs/reference/create-form).-> All three action |
||
| version: "0.0.1", | ||
| type: "action", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: true, | ||
| readOnlyHint: false, | ||
| }, | ||
| props: { | ||
| emboss, | ||
| file: { | ||
| type: "string", | ||
| label: "File Path Or Url", | ||
| description: "Provide either a file URL or a path to a file in the `/tmp` directory (for example, `/tmp/form.pdf`).", | ||
| format: "file-ref", | ||
| }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add optional props "Idempotency-Key" and "callback_url" |
||
| syncDir: { | ||
| type: "dir", | ||
| accessMode: "read-write", | ||
| sync: true, | ||
| }, | ||
| }, | ||
| async run({ $ }) { | ||
| const { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The action's core artifact - a downloadable 'fillable PDF' via getFillablePdf() → GET /forms/{formId}/fillable - cannot be verified. The Emboss API reference documents POST /forms (explicitly 'Detect fields only (no fill)') and GET /forms/{id} and GET /forms/{id}/contract, but NO /forms/{id}/fillable endpoint anywhere. POST /forms produces a structured field contract (JSON), not a fillable interactive PDF, so both the endpoint and the action's stated output shape are unconfirmed against the docs. Confirm this endpoint exists (it may be undocumented) or rework the action around the documented contract/session model.-> https://getemboss.ai/docs/reference/create-form (documents only POST /forms, GET /forms/{id}); https://getemboss.ai/docs/reference/get-contract (GET /forms/{id}/contract returns field JSON, not a PDF); quickstart 'Pick your endpoint' table: 'Detect fields only (no fill) | POST /forms'. No /forms/{id}/fillable in any API Reference page. |
||
| runs, context, | ||
| } = $.context.run; | ||
| if (runs === 1) { | ||
| if (!this.file) { | ||
| throw new ConfigurationError("File Path Or Url is required."); | ||
| } | ||
| const f = await resolveFileRef(this.file, "form.pdf"); | ||
| const form = new FormData(); | ||
| form.append("file", f.stream, { | ||
| filename: f.filename, | ||
| contentType: "application/pdf", | ||
| knownLength: f.size, | ||
| }); | ||
| const created = await this.emboss.createForm({ | ||
| $, | ||
| headers: form.getHeaders(), | ||
| data: form, | ||
| maxBodyLength: Infinity, | ||
| }); | ||
| $.flow.rerun(POLL_DELAY_MS, { | ||
| form_id: created.form_id, | ||
| }, MAX_RETRIES); | ||
| $.export("$summary", `Processing PDF — Emboss form \`${created.form_id}\` queued`); | ||
| return; | ||
| } | ||
| const { form_id: formId } = context; | ||
| const status = await this.emboss.getForm({ | ||
| $, | ||
| formId, | ||
| }); | ||
| if (status.status === "failed") { | ||
| throw new Error(`Emboss create failed: ${errorDetail(status.error)}`); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use configuration Error to throw the error status |
||
| } | ||
| if (status.status !== "ready") { | ||
| if (runs >= MAX_RETRIES) { | ||
| throw new Error("Emboss job still processing after the polling limit (~12 minutes) — re-run with a smaller PDF or check the job in your Emboss dashboard."); | ||
| } | ||
| $.flow.rerun(POLL_DELAY_MS, { | ||
| form_id: formId, | ||
| }, MAX_RETRIES); | ||
| $.export("$summary", `Still processing form \`${formId}\` (check ${runs} of ${MAX_RETRIES})`); | ||
| return; | ||
| } | ||
| const pdf = await this.emboss.getFillablePdf({ | ||
| $, | ||
| formId, | ||
| }); | ||
| const { filepath } = await writePdf(Buffer.from(pdf), `emboss-${formId}.pdf`); | ||
| $.export("$summary", `Successfully created fillable form \`${formId}\``); | ||
| return { | ||
| form_id: formId, | ||
| status: "ready", | ||
| filepath, | ||
| }; | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import FormData from "form-data"; | ||
| import { ConfigurationError } from "@pipedream/platform"; | ||
| import emboss from "../../emboss.app.mjs"; | ||
| import { | ||
| resolveFileRef, contextParts, writePdf, errorDetail, | ||
| } from "../../common/utils.mjs"; | ||
|
|
||
| const POLL_DELAY_MS = 5000; | ||
| const MAX_RETRIES = 144; | ||
|
|
||
| export default { | ||
| key: "emboss-fill-existing-form", | ||
| name: "Fill Existing Form", | ||
| description: "Fill a form you previously created in Emboss, using context text and/or a context file; Emboss populates the detected fields from your context and returns the completed PDF. Create the form first with **Create Fillable Form**. Provide at least one context input. Polls up to ~12 minutes. [See the documentation](https://getemboss.ai/docs)", | ||
| version: "0.0.1", | ||
| type: "action", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: true, | ||
| readOnlyHint: false, | ||
| }, | ||
| props: { | ||
| emboss, | ||
| formId: { | ||
| propDefinition: [ | ||
| emboss, | ||
| "formId", | ||
| ], | ||
| }, | ||
| contextText: { | ||
| type: "string", | ||
| label: "Context (Text)", | ||
| description: "Information to fill the form with, e.g. `Applicant: Jane Doe, 38 Birchwood Court, Mooresville, IN — phone (317) 555-0126`.", | ||
| optional: true, | ||
| }, | ||
| contextFile: { | ||
| type: "string", | ||
| label: "Context File", | ||
| description: "A URL or `/tmp` path to a context document (PDF, DOCX, CSV, image, or text), e.g. `/tmp/customer-data.pdf` or `https://example.com/invoice.pdf`.", | ||
| format: "file-ref", | ||
| optional: true, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }, | ||
| syncDir: { | ||
| type: "dir", | ||
| accessMode: "read-write", | ||
| sync: true, | ||
| }, | ||
| }, | ||
| async run({ $ }) { | ||
| const { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After the context job reaches |
||
| runs, context, | ||
| } = $.context.run; | ||
| if (runs === 1) { | ||
| if (!this.contextText && !this.contextFile) { | ||
| throw new ConfigurationError("Provide Context (Text) and/or a Context File — at least one is required to fill the form."); | ||
| } | ||
| const cf = await resolveFileRef(this.contextFile, "context"); | ||
| const form = new FormData(); | ||
| for (const p of contextParts(this.contextText, cf)) { | ||
| form.append("context", p.value, { | ||
| filename: p.filename, | ||
| contentType: p.contentType, | ||
| knownLength: p.knownLength, | ||
| }); | ||
| } | ||
| const created = await this.emboss.fillExistingForm({ | ||
| $, | ||
| formId: this.formId, | ||
| headers: form.getHeaders(), | ||
| data: form, | ||
| maxBodyLength: Infinity, | ||
| }); | ||
| $.flow.rerun(POLL_DELAY_MS, { | ||
| job_id: created.job_id, | ||
| }, MAX_RETRIES); | ||
| $.export("$summary", `Filling form — Emboss job \`${created.job_id}\` queued`); | ||
| return; | ||
| } | ||
| const { job_id: jobId } = context; | ||
| const status = await this.emboss.getContextJob({ | ||
| $, | ||
| jobId, | ||
| }); | ||
| if (status.status === "failed") { | ||
| throw new Error(`Emboss fill failed: ${errorDetail(status.error)}`); | ||
| } | ||
| if (status.status !== "ready") { | ||
| if (runs >= MAX_RETRIES) { | ||
| throw new Error("Emboss job still processing after the polling limit (~12 minutes) — re-run with a smaller PDF or check the job in your Emboss dashboard."); | ||
| } | ||
| $.flow.rerun(POLL_DELAY_MS, { | ||
| job_id: jobId, | ||
| }, MAX_RETRIES); | ||
| $.export("$summary", `Still filling (check ${runs} of ${MAX_RETRIES})`); | ||
| return; | ||
| } | ||
| await this.emboss.fillSession({ | ||
| $, | ||
| sessionId: status.session_id, | ||
| }); | ||
| const pdf = await this.emboss.getSessionPdf({ | ||
| $, | ||
| sessionId: status.session_id, | ||
| }); | ||
| const { filepath } = await writePdf(Buffer.from(pdf), `emboss-${status.session_id}.pdf`); | ||
| $.export("$summary", `Successfully filled form \`${this.formId}\` (session \`${status.session_id}\`)`); | ||
| return { | ||
| session_id: status.session_id, | ||
| report: status.report || {}, | ||
| filepath, | ||
| }; | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import FormData from "form-data"; | ||
| import { ConfigurationError } from "@pipedream/platform"; | ||
| import emboss from "../../emboss.app.mjs"; | ||
| import { | ||
| resolveFileRef, contextParts, writePdf, errorDetail, | ||
| } from "../../common/utils.mjs"; | ||
|
|
||
| const POLL_DELAY_MS = 5000; | ||
| const MAX_RETRIES = 144; | ||
|
|
||
| export default { | ||
| key: "emboss-fill-from-pdf-context", | ||
| name: "Fill PDF From Context", | ||
| description: "Upload a flat (non-fillable) PDF plus context (text and/or a file); Emboss detects the fields and fills them with AI in one step, returning the completed PDF. Provide at least one context input. Use **Create Fillable Form** + **Fill Existing Form** instead to reuse the same form repeatedly. Polls up to ~12 minutes for large documents. [See the documentation](https://getemboss.ai/docs)", | ||
| version: "0.0.1", | ||
| type: "action", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: true, | ||
| readOnlyHint: false, | ||
| }, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| props: { | ||
| emboss, | ||
| file: { | ||
| type: "string", | ||
| label: "File Path Or Url", | ||
| description: "Provide either a file URL or a path to a file in the `/tmp` directory (for example, `/tmp/form.pdf`).", | ||
| format: "file-ref", | ||
| }, | ||
| contextText: { | ||
| type: "string", | ||
| label: "Context (Text)", | ||
| description: "Information to fill the form with, e.g. `Applicant: Jane Doe, 38 Birchwood Court, Mooresville, IN — phone (317) 555-0126`.", | ||
| optional: true, | ||
| }, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| contextFile: { | ||
| type: "string", | ||
| label: "Context File", | ||
| description: "A URL or `/tmp` path to a context document (PDF, DOCX, CSV, image, or text), e.g. `/tmp/customer-data.pdf` or `https://example.com/invoice.pdf`.", | ||
| format: "file-ref", | ||
| optional: true, | ||
| }, | ||
| syncDir: { | ||
| type: "dir", | ||
| accessMode: "read-write", | ||
| sync: true, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add optional: true to syncDir. Keep accessMode: "read-write"; the |
||
| }, | ||
| }, | ||
| async run({ $ }) { | ||
| const { | ||
| runs, context, | ||
| } = $.context.run; | ||
| if (runs === 1) { | ||
| if (!this.file) { | ||
| throw new ConfigurationError("File Path Or Url is required."); | ||
| } | ||
| if (!this.contextText && !this.contextFile) { | ||
| throw new ConfigurationError("Provide Context (Text) and/or a Context File — at least one is required to fill the form."); | ||
| } | ||
| const f = await resolveFileRef(this.file, "form.pdf"); | ||
| const cf = await resolveFileRef(this.contextFile, "context"); | ||
| const form = new FormData(); | ||
| form.append("file", f.stream, { | ||
| filename: f.filename, | ||
| contentType: "application/pdf", | ||
| knownLength: f.size, | ||
| }); | ||
| for (const p of contextParts(this.contextText, cf)) { | ||
| form.append("context", p.value, { | ||
| filename: p.filename, | ||
| contentType: p.contentType, | ||
| knownLength: p.knownLength, | ||
| }); | ||
| } | ||
| const created = await this.emboss.createWithContext({ | ||
| $, | ||
| headers: form.getHeaders(), | ||
| data: form, | ||
| maxBodyLength: Infinity, | ||
| }); | ||
| $.flow.rerun(POLL_DELAY_MS, { | ||
| job_id: created.job_id, | ||
| }, MAX_RETRIES); | ||
| $.export("$summary", `Filling form — Emboss job \`${created.job_id}\` queued`); | ||
| return; | ||
| } | ||
| const { job_id: jobId } = context; | ||
| const status = await this.emboss.getContextJob({ | ||
| $, | ||
| jobId, | ||
| }); | ||
| if (status.status === "failed") { | ||
| throw new Error(`Emboss fill failed: ${errorDetail(status.error)}`); | ||
| } | ||
| if (status.status !== "ready") { | ||
| if (runs >= MAX_RETRIES) { | ||
| throw new Error("Emboss job still processing after the polling limit (~12 minutes) — re-run with a smaller PDF or check the job in your Emboss dashboard."); | ||
| } | ||
| $.flow.rerun(POLL_DELAY_MS, { | ||
| job_id: jobId, | ||
| }, MAX_RETRIES); | ||
| $.export("$summary", `Still filling (check ${runs} of ${MAX_RETRIES})`); | ||
| return; | ||
| } | ||
| await this.emboss.fillSession({ | ||
| $, | ||
| sessionId: status.session_id, | ||
| }); | ||
| const pdf = await this.emboss.getSessionPdf({ | ||
| $, | ||
| sessionId: status.session_id, | ||
| }); | ||
| const { filepath } = await writePdf(Buffer.from(pdf), `emboss-${status.session_id}.pdf`); | ||
| $.export("$summary", `Successfully filled form (session \`${status.session_id}\`)`); | ||
| return { | ||
| session_id: status.session_id, | ||
| report: status.report || {}, | ||
| filepath, | ||
| }; | ||
| }, | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.