Skip to content

Worker Upload

From an F# program, upload the JavaScript module that your Worker compiles to. The upload's metadata declares the Worker's bindings, and separate calls set its schedule and its workers.dev address.

Hello Worker

WorkerScriptUploadWorkerModule sends a multipart request with a metadata part and one part per module. In the metadata, main_module identifies the part with the entry module, and the file's PartName matches it.

open System.IO
open System.Text.Json
open FSharp.CloudEdge.Core.Api.Http
open FSharp.CloudEdge.Core.Api.Types
open FSharp.CloudEdge.Management.Compute

let upload (compute: ComputeClient) accountId =
    task {
        let workerModule: MultipartFile =
            { Bytes = File.ReadAllBytes "dist/worker.js"
              FileName = "worker.js"
              ContentType = Some "application/javascript+module"
              PartName = Some "worker.js" }
        let metadata =
            JsonSerializer.SerializeToNode({| main_module = "worker.js"; compatibility_date = "2026-09-06" |}).AsObject()
        match! compute.WorkerScriptUploadWorkerModule(accountId, "hello", metadata, files = [ workerModule ]) with
        | WorkerScriptUploadWorkerModule.OK payload ->
            printfn "hello uploaded, startup %d ms" payload.result.startup_time_ms
            printfn "Handlers: %A" payload.result.handlers
        | WorkerScriptUploadWorkerModule.Status4XX(status, _) ->
            printfn "Upload rejected with HTTP %d" status
    }

Needs your Worker's JavaScript module at dist/worker.js. First Worker covers the compile step.

Guestbook Bindings

Bindings are JSON objects in the metadata's bindings array. Anonymous records serialize with their field names as JSON keys, and the field type requires double backticks because type is an F# keyword. Each binding's name is its JavaScript variable name inside the Worker.

open System.Text.Json
open System.Text.Json.Nodes

let metadata (databaseId: string) (namespaceId: string) =
    let bindings: JsonNode[] =
        [| JsonSerializer.SerializeToNode {| ``type`` = "d1"; name = "DB"; database_id = databaseId |}
           JsonSerializer.SerializeToNode {| ``type`` = "kv_namespace"; name = "SESSIONS"; namespace_id = namespaceId |}
           JsonSerializer.SerializeToNode {| ``type`` = "r2_bucket"; name = "PHOTOS"; bucket_name = "photos" |}
           JsonSerializer.SerializeToNode {| ``type`` = "queue"; name = "ORDERS"; queue_name = "orders" |}
           JsonSerializer.SerializeToNode {| ``type`` = "plain_text"; name = "SITE_TITLE"; text = "Guestbook" |} |]
    let settings =
        {| main_module = "worker.js"
           compatibility_date = "2026-09-06"
           bindings = bindings |}
    JsonSerializer.SerializeToNode(settings).AsObject()

A D1 binding takes database_id, which Cloudflare's upload reference lists in place of the deprecated id.

Needs the storage and the queue from Account Setup.

Chat Room

A Durable Object binding refers to the exported class by class_name. The first upload with the class includes a migration, and Cloudflare creates a SQLite-backed namespace for each class in new_sqlite_classes.

open System.Text.Json
open System.Text.Json.Nodes

let metadata () =
    let rooms: JsonNode =
        JsonSerializer.SerializeToNode {| ``type`` = "durable_object_namespace"; name = "ROOMS"; class_name = "ChatRoom" |}
    let settings =
        {| main_module = "worker.js"
           compatibility_date = "2026-09-06"
           bindings = [| rooms |]
           migrations = {| new_tag = "v1"; new_sqlite_classes = [| "ChatRoom" |] |} |}
    JsonSerializer.SerializeToNode(settings).AsObject()

For a later migration, set old_tag to the current migration tag and new_tag to the next one. Cloudflare rejects the upload when old_tag differs from the latest one.

Needs a Worker module that exports the ChatRoom class. Durable Objects has such classes in F#.

Payment Key

The program reads the key from an environment variable and uploads it in a secret_text binding. keep_bindings is the list of binding types to keep from the previous upload. With secret_text in that list, only the first upload includes the key.

open System
open System.Text.Json
open System.Text.Json.Nodes

let firstUpload () =
    let apiKey = Environment.GetEnvironmentVariable "PAYMENTS_API_KEY"
    let secret: JsonNode =
        JsonSerializer.SerializeToNode {| ``type`` = "secret_text"; name = "PAYMENTS_API_KEY"; text = apiKey |}
    let settings =
        {| main_module = "worker.js"
           compatibility_date = "2026-09-06"
           bindings = [| secret |] |}
    JsonSerializer.SerializeToNode(settings).AsObject()

let laterUpload () =
    let settings =
        {| main_module = "worker.js"
           compatibility_date = "2026-09-06"
           keep_bindings = [| "secret_text" |] |}
    JsonSerializer.SerializeToNode(settings).AsObject()

Nightly Cleanup

WorkerCronTriggerUpdateCronTriggers takes the schedules as cron expressions. Cron Triggers run on UTC time, so this schedule fires at 03:00 UTC every day.

open FSharp.CloudEdge.Core.Api.Types
open FSharp.CloudEdge.Management.Compute

let schedule (compute: ComputeClient) accountId =
    task {
        let nightly = workers_schedule.Create "0 3 * * *"
        match! compute.WorkerCronTriggerUpdateCronTriggers(accountId, "guestbook", [ nightly ]) with
        | WorkerCronTriggerUpdateCronTriggers.OK payload ->
            for trigger in payload.result.schedules do
                printfn "guestbook runs on %s" trigger.cron
        | WorkerCronTriggerUpdateCronTriggers.Status4XX(status, failure) ->
            for error in failure.errors do
                printfn "HTTP %d: %s" status error.message
    }

The Worker receives each run in the scheduled handler of its ExportedHandler. Per Cloudflare's documentation, a schedule change can take up to 15 minutes to propagate.

Public URL

WorkerScriptPostSubdomain turns on the Worker's workers.dev route. The address has the form <worker>.<subdomain>.workers.dev, and WorkerSubdomainGetSubdomain returns the account's subdomain.

open FSharp.CloudEdge.Core.Api.Types
open FSharp.CloudEdge.Management.Compute

let publish (compute: ComputeClient) accountId (scriptName: string) =
    task {
        let enable = WorkerScriptPostSubdomainPayload.Create true
        match! compute.WorkerScriptPostSubdomain(accountId, scriptName, enable) with
        | WorkerScriptPostSubdomain.OK _ ->
            match! compute.WorkerSubdomainGetSubdomain(accountId) with
            | WorkerSubdomainGetSubdomain.OK account ->
                printfn "https://%s.%s.workers.dev" scriptName account.result.subdomain
            | WorkerSubdomainGetSubdomain.Status4XX(status, _) ->
                printfn "Subdomain lookup returned HTTP %d" status
        | WorkerScriptPostSubdomain.Status4XX(status, _) ->
            printfn "workers.dev setting returned HTTP %d" status
    }

Order Consumer

A Worker consumer receives the queue's messages in its queue handler. QueuesCreateConsumer takes the queue ID from Account Setup and a consumer request, where Variant1 is the Worker form and Variant2 the HTTP pull form.

open FSharp.CloudEdge.Core.Api.Types
open FSharp.CloudEdge.Management.Compute

let attach (compute: ComputeClient) accountId queueId =
    task {
        let worker =
            mq_worker_u002D_consumer_u002D_request.Create("guestbook", mq_worker_u002D_consumer_u002D_requestType.Worker)
        match! compute.QueuesCreateConsumer(queueId, accountId, body = mq_consumer_u002D_request.Variant1 worker) with
        | QueuesCreateConsumer.OK _ ->
            printfn "guestbook now consumes the orders queue"
        | QueuesCreateConsumer.Status4XX(status, failure) ->
            for error in Option.defaultValue [] failure.errors do
                printfn "HTTP %d: %s" status error.message
    }

The request refers to the Worker by script_name, so the program attaches the consumer after the upload.

Binding Kinds

Each row is one entry in the bindings array. The keys follow the binding schemas in Cloudflare's OpenAPI document.

Binding type Keys
D1 database d1 name, database_id
KV namespace kv_namespace name, namespace_id
R2 bucket r2_bucket name, bucket_name
Queue producer queue name, queue_name
Durable Object durable_object_namespace name, class_name
Workflow workflow name, workflow_name, class_name
Workers AI ai name
AI Search namespace ai_search_namespace name, namespace
Vectorize index vectorize name, index_name
Analytics Engine analytics_engine name, dataset
Hyperdrive hyperdrive name, id
Service service name, service
Browser Rendering browser name
Images images name
Environment variable plain_text name, text
Secret secret_text name, text

Library Table

Library Operations What it covers
Management.Compute 304 Uploads, schedules, queues, workers.dev
Core.Api 0 MultipartFile and the response types

NuGet packages

Core.Api 0.1.0, Management.Compute 0.1.0.

See installation and release availability.

Edit this page