Skip to main content
Welcome to AbstraOverviewAuthenticationOverviewTreasury DashboardAuthenticated Task ManagerExpense Submission FormSystem Status MonitorFiltered Orders TableStreaming Progress BarStreaming File DownloadStreaming Video with Media Source ExtensionsFile uploads in Pagesget_query_paramsget_userregister_staticOverviewBasicsExternal DataReactive PagesPre-filled fieldsRedirectCustom ValidationProgress BarCustom ButtonsGetting User InformationQuery ParametersCRUD with DatabaseClient-side JavaScriptOverviewAppointmentInputCardsInputCheckboxInputChecklistInputCnpjInputCodeInputCpfInputCurrencyInputCustomInputDateInputDropdownInputEmailInputOverviewCameraInputImageInputVideoInputListInputMultipleChoiceInputNpsInputNumberInputNumberSliderInputPandasRowSelectionInputPasswordInputPhoneInputRatingInputRichTextInputRowTagInputTextInputTextareaInputTimeInputToggleInputOverviewFileOutputHtmlOutputIframeOutputImageOutputLatexOutputLinkOutputMarkdownOutputPandasOutputPlotlyOutputProgressOutputTextOutputStep TypesAuthenticationForm SettingsNotificationsHome Page and SidebarOverviewBackButtonExitButtonNextButtonend_page_stepexecute_jsget_query_paramsget_userListItemSchemarunWidgetOverviewBasic HookReceiving Connector EventsReceiving EmailsTriggering Hooksget_email_requestget_raw_requestget_requestsend_jsonsend_responseJobsOverviewSending a taskGetting and using TasksIterate through tasksGet trigger task (Tasklet)TaskletsTask Schema Validationget_sent_tasksget_tasksget_trigger_taskiter_taskssend_taskTaskOverviewPrompt ConstructionToolsExamplesWorkflowAgentsPromptingFixed answer formatReading an InvoiceParsing DocumentsOverviewBrowserToolsConnectorsToolsFilesToolsTablesToolsextract_textparse_bank_statementparse_boletoparse_invoiceparse_nfeparse_nfseparse_oficiosparse_us_driver_licenseparse_us_passportpromptrun_agentOverviewCreating and Loading FilesPublic Filesget_persistent_dirget_public_dirOverviewThe custom actionrun_connection_actionDatabricksItaú ExtratosOverviewConsultasEmissão de NFS-eEventos de NFS-eTroubleshootingGoogle DriveOverviewEnvio de Lote de RPS (Versão 2)Itaú VAN (SFTP)OverviewReading dataSupplier invoice (MIRO)Bank statementTransactional sessionsValue helpsTroubleshootingAbstra Cash ConnectOverviewIssuing an NF-eEvents: Cancellation and Correction LetterTroubleshootingBlingBradescoOverviewAttaching Files to NetSuite RecordsRetrieving and Updating Files in NetSuiteSearching Files Attached to RecordsSearching Files in NetSuite File CabinetUploading Files to NetSuite File CabinetCreating Custom Reports with NetSuite Purchase OrdersCreating Purchase Orders and Attaching Files in NetSuiteFiltering Purchase Orders by Custom FieldsReading Purchase Order Line ItemsReading Purchase Order DetailsSearching and Filtering Purchase OrdersOverviewGetting Invoice Details and TransactionsListing and Searching Invoices99Abstra Manager APIAbstra Project APIADN NFS-eAdyenAnchorage DigitalAsaasAmazon Web ServicesMicrosoft AzureBanco ABCBanco do BrasilBanrisulBase ChainBanco Central do BrasilBetha e-Nota CloudBigDataCorpBinanceBitstampBlingBotConversaBradescoBrasil APIBrazeBrexBS2BTG Pactual Empresas (Custom App)BTG Pactual EmpresasAbstra Cash ConnectCelcoinCerttaCielo e-CommerceCielo EDI LinkCielo Extrato EletrônicoCieloClaraClicksignClickUpCNJ - DataJudCNJ - Domicílio Judicial EletrônicoCNJ - Diário de Justiça Eletrônico NacionalCobranSaaSCoinbase CDPCoinbase Commerce (Business)Coinbase Exchange / Prime / INTX / Derivatives (HMAC)ConfluenceConfluence (API Token)Conta AzulConta SimplesMicrosoft Copilot Studio (Custom App)CresolCRS 360Databricks AccountDatabricks WorkspaceDatatravelData Rudder (DeLorean)DFNSDiscordDocuSign (JWT)DocuSignDynamics 365 Business CentraleNotas GatewayEquals ConnectEthereumFirecrawlFiservFlashFlukeFoxbitFreshserviceGoogle Cloud PlatformGitHub (Personal Access Token)GitHubGmailGoogle AnalyticsGoogle BigQuery (Service Account)Google BigQueryGoogle Calendar (Service Account)Google CalendarGoogle Chat (Service Account)Google ChatGoogle Docs (Service Account)Google DocsGoogle Drive (Service Account)Google DriveGoogle Maps (Service Account)Google Maps (API Key)Google MapsGoogle Sheets (Service Account)Google SheetsGranatumGRF/OFMHi PlatformHolmesHubSpotIBGEiFoodInfosimplesBanco InterIntercomItaú BoletosItaú ExtratosItaú Pagamentos (SISPAG)Itaú PixItaú VAN (SFTP)IuguJD Pagamentos InstantâneosJiraJira (API Token)KommoKraken DerivativesKraken SpotLinkedInLinx OMALitecoinLookerMagaluMailchimpMCPMega ERPMercado BitcoinMercado LivreMercado PagoMercosMetabaseMicrosoft 365 Copilot (Custom App)Microsoft BookingsMicrosoft ExcelMicrosoft OneNoteMicrosoft Outlook (Service Principal)Microsoft OutlookMicrosoft PlannerMicrosoft SharePoint + OneDrive (Service Principal)Microsoft SharePoint + OneDriveMicrosoft Teams (Service Principal)Microsoft TeamsMicrosoft To DoMonday.comMXM WebManagerMySQLNektnetLexNetSuite OAuth2NetSuite TBANexxera - Nix EmpresasNexxera - Nix PayNF-e São PauloNordVPNNota BarueriNota PaulistanaNota SalvadorNotionNotion (API Token)NuPayOKXOlist EcommerceOlist ERPOmie ERPOneStreamOnFlyOracle E-Business SuiteOracle DatabasePagarePagar.mePagBank EDIPagSeguroPayPalPipedrivePipedrive (API Token)PipefyPodioPolygonPostgreSQLPostmanProJuris EmpresasProJurisQiveQuickBooksRappiRDP (Windows Remote)Recall.aiReceita Federal - Apuração Assistida CBSReceiv BIRedecard (e.Rede)Amazon RedshiftRevolut BusinessRevolut MerchantRevolut Open BankingSabespSafeguardSalesforceSankhyaSantanderSAP ABAP Development Tools (ADT)SAP Cloud ConnectorSAP ConcurSAP CPI (Integration Suite)S/4HANASAP Service LayerSBA CertificationsSentrySerproSerpro Integra ContadorSFTPShopify StorefrontShopifySicoobSicrediSiengeBanco Central do Brasil - SiscomSlack (API Token)SlackSnowflakeSolanaSQL ServerBanco Central do Brasil - STA (Sistema de Transferência de Arquivos)Stark BankStripe (API Key)StripeSVB (Silicon Valley Bank)Takotl;dvTodoistTotal BankTotvsTravellinkConsumo de Tributos GovTRONTypeformTyxterUAUWeb (Globaltec)UberVERT CapitalVindiVórtx VxRecebíveisVórtxVTEXWhatsApp BusinessWhatsApp Business (API Token)WhatsApp (RPA)WoofficeWoopayXP InvestimentosXRP Ledger (Ripple)YunoZé DeliveryZendeskZendesk (API Token)OverviewInserting rowsSelecting rowsUpdating rowsDeleting dataUsing SQLUsing comparatorsBackup & RestoreUploading CSV filesdelete_by_idinsertis_betweenis_eqis_gtis_gteis_ilikeis_inis_likeis_ltis_lteis_neqis_not_ilikeis_not_inis_not_likeis_not_nullis_nullquery_dfrunrun_sqlselectselect_by_idselect_dfselect_oneTablesExecutionErrorupdateupdate_by_idBuilt-in TablesOverviewDeclaring actionsabstra.servicesOverviewExamplesOverviewSingle Sign-On (SSO)Enviroment VariablesOverviewUsing Abstra LoggerOverviewExamplessend_emailUI PreferencesRetrieving Project URLRetrieving Run IDExternal DebuggerSetting up CIEditor doesn't startUsing Legacy Threads
Cloud Console

File uploads in Pages

Use htmx for a form that returns HTML, or the generated JS function for structured results or per-file progress. Both deliver the same file dictionary to Python. Do not manually implement the old RPC multipart format.

The picker is the design system's ab-upload: drag and drop, keyboard, type and size validation and a file list. It is a native form field, so a <form> (and htmx) submits its files under its name.

Upload form with htmx​

First install the Jinja helper and htmx 4.0.0 base template, with its endpoint/auth hook. Save the HTML blocks below as templates/upload.html and templates/upload_result.html.

from pathlib import Path from uuid import uuid4 from abstra.pages import register_function from abstra.common import get_persistent_dir from lib_jinja import render_template @register_function def post_upload_file(file: dict): safe_name = Path(file["filename"]).name if not safe_name: return render_template("upload_result.html", error="Choose a named file.") destination = get_persistent_dir() / "uploads" / str(uuid4()) / safe_name destination.parent.mkdir(parents=True, exist_ok=True) destination.write_bytes(file["content"]) return render_template("upload_result.html", name=safe_name, size=len(file["content"])) @register_function def __render__(): return render_template("upload.html", title="File upload")
{% extends "base.html" %} {% block body %} <ab-page-header heading="File upload"></ab-page-header> <ab-stack gap="4"> <form hx-post="_fn/upload_file" hx-encoding="multipart/form-data" hx-target="#result"> <ab-stack gap="4"> <ab-upload name="file" label="Document" accept=".pdf,.csv,.xlsx" max-size="10" required></ab-upload> <ab-cluster justify="end"> <ab-button type="submit" variant="primary" icon="upload-simple">Upload</ab-button> </ab-cluster> </ab-stack> </form> <div id="result" role="status"></div> </ab-stack> {% endblock %}
{% if error %} <ab-alert tone="danger">{{ error }}</ab-alert> {% else %} <ab-alert tone="ok">Uploaded {{ name }} ({{ size }} bytes).</ab-alert> {% endif %}

POST _fn/upload_file calls post_upload_file. The submit ab-button shows its loading state while the file uploads, and accept/max-size reject wrong files in the browser before any request (validate again in Python). Do not repeat post_ in the URL. Return an HTML string for htmx, not a dict (which gives 406). User-supplied filenames are escaped by Jinja. The unique subdirectory prevents unrelated uploads with the same name from overwriting one another.

File contract in Python​

A file arrives as a dict, not a Path or file-like object:

KeyTypeExample
filenamestr"report.pdf"
content_typestr"application/pdf"
contentbytesRaw file bytes

The whole file is in memory before the function runs. If an API requires a filesystem path, save it with Path.write_bytes(file["content"]). Always sanitize the filename before using it in a path. Do not call file.name, file.read() or shutil.copy2(file, ...). Use dict or no annotation; the runtime does not convert annotations.

Other multipart fields​

Ordinary fields pass through json.loads when possible: id=7 becomes an integer, true becomes a boolean, and numeric postal codes may lose leading zeroes. Handle those types explicitly; urlencoded forms remain text, but multipart does not promise that. Use JS RPC/JSON when preserving structured types matters.

Multipart query parameters stay available via get_query_params(), not as function arguments. Do not supply __function__ yourself for /_fn requests; cloud-api adds the resolved name at the end. Malformed multipart delimiters return 400.

Multiple files​

Repeated file parts under one name produce a list of dictionaries, but one file produces a single dictionary. Normalize both cases:

@register_function def post_upload_many(files): items = files if isinstance(files, list) else [files] return f'<ab-alert tone="ok">Received {len(items)} file(s).</ab-alert>'
<form hx-post="_fn/upload_many" hx-encoding="multipart/form-data" hx-target="#result"> <ab-stack gap="4"> <ab-upload name="files" label="Receipts" multiple max-files="10" required></ab-upload> <ab-cluster justify="end"><ab-button type="submit" variant="primary">Upload</ab-button></ab-cluster> </ab-stack> </form> <div id="result" role="status"></div>

Generated JavaScript RPC remains supported​

When a File or Blob is passed to a generated function, the wrapper switches to multipart automatically, supplies __function__, and JSON-encodes non-file arguments. Otherwise it sends JSON. This path POSTs to the page endpoint, not /_fn, and keeps its literal function names.

from pathlib import Path from uuid import uuid4 from abstra.pages import register_function from abstra.common import get_persistent_dir @register_function def upload_file(file: dict): safe_name = Path(file["filename"]).name destination = get_persistent_dir() / "uploads" / str(uuid4()) / safe_name destination.parent.mkdir(parents=True, exist_ok=True) destination.write_bytes(file["content"]) return {"name": safe_name, "path": str(destination)}
const upload = document.querySelector('ab-upload'); const result = await upload_file(upload.files[0]);

Pass the actual File/Blob, not just its name or path. Mixed structured arguments work if they are declared in the Python signature. For per-file status, call the function once per file and report each one back to ab-upload, which shows a progress bar, a check or the error under each file:

for (const file of upload.files) { upload.setStatus(file, "uploading"); try { await upload_file(file); upload.setStatus(file, "done"); } catch (error) { upload.setStatus(file, "error", { message: await abErrorMessage(error) }); } }

A structured return value such as {"name": str, "path": str} remains useful with RPC (the same convention as Forms' FileResponse). It is not an HTML response for htmx. Returning raw bytes is not supported: use a URL, base64 plus a browser Blob, or register_static(path) for downloads.

Request size and troubleshooting​

Uploads use the regular execution pipeline. Large files can hit transport limits and are fully loaded into memory. For very large files, upload directly to object storage and pass its URL/key to the function instead. There is no streaming/chunked file parameter API.

SymptomCheck
404 on _fn/upload_fileThe POST function must be named post_upload_file and registered.
406 with htmxReturn an HTML string, not a structured object.
Missing fileSet name="file" on the ab-upload (without name it submits nothing), hx-encoding="multipart/form-data", and match the Python parameter.
Unexpected integer fieldMultipart fields may be JSON-decoded; convert deliberately.
Function 'None' not found with old RPCDo not hand-roll its multipart body; call the generated wrapper.
'dict' object has no attribute 'name'Use file["filename"] and file["content"].
Upload appears to hangCheck the request size and memory requirements.