1. 🧩 Server Scripts: Running Reports via Perform Script on Server

  1. 🧩 Server Scripts: Running Reports via Perform Script on Server
    1. 📁 Where can we store files generated from reports on the server?
      1. 🧩 Example: Creating a report file path in ACF
    2. 💾 Storing the report in a container
    3. ⚙️ The ACF Way – inserting the file with a function
      1. Example ACF function
    4. 📤 Example: Server-side report export script
    5. 💻 Client-side script that runs the report
    6. 📂 Retrieving and managing the report
    7. Special considerations for PSOS and the OnFirstWindowOpen trigger

When the ACF Plugin is installed on the FileMaker Server, you can generate reports (Excel, CSV, XML, etc.) directly on the server — completely unattended. But there’s an important question:

How do we make the generated report accessible to the client, since it’s stored on the server and not easily reachable from FileMaker Pro?

There are several ways to handle this. One of the simplest and most secure methods is to store the generated report in a container field.


1.1. 📁 Where can we store files generated from reports on the server?

The FileMaker Server documentation from Claris is very clear on this point: The only supported location for storing user-generated files from server scripts is the Documents folder on the server — or any subfolder within it.

The exact location can vary depending on the installation, but calling:

Get ( DocumentsPath )

on the server will return the correct path for that installation.

Alternatively, you can use the temporary folder, defined by:

Get ( TemporaryPath )

executed on the server. Files stored there will be deleted automatically at regular intervals or the server restarts.


1.1.1. 🧩 Example: Creating a report file path in ACF

Inside an ACF function, you can safely create a path for storing a report like this:

string path = documents_directory() + "AnySubFolder/";
string res = create_directory(path); // Ensure the subfolder exists
path += format("ReportName_%s.xlsx", string(now(), "%Y-%m-%d_%H%M%S"));

This code builds a valid and unique filename for your generated Excel report, stored inside Documents/AnySubFolder/ with a timestamp appended to the name (e.g., ReportName_2025-10-20_001412.xlsx).


1.2. 💾 Storing the report in a container

Create a table, for example ServerReports, where the server script can:

However, the regular Insert File script step is not available on the server. Instead, you could use Insert from URL, which is server-compatible. This requires that your file path starts with file:/// followed by the absolute file path, with any spaces URL-encoded (for example, %20).


1.3. ⚙️ The ACF Way – inserting the file with a function

A simpler and more flexible approach is to let the ACF plugin handle the insertion. You can define a small ACF function that loads a file into a container and returns it directly to FileMaker.

1.3.1. Example ACF function

function MakeContainerFromPath (string path)

    // Ask for file if no path is provided (only available on client)
    if (!isServer && path == "") then
        path = select_file("Select the file for the container", "");
    end if

    if (path == "") then
        throw "No file specified";
    end if

    if (!file_exists(path)) then
        throw "File does not exist: " + path;
    end if

    // We have a valid path — load it into a container and return it
    container doc;
    Container_LoadFile(doc, path);
    return doc;
end

This function can now be used from a Set Field script step to insert the file into a container — even in server scripts.


1.4. 📤 Example: Server-side report export script

At the end of your report-generation function, store the generated file path in a global variable:

$$FilePath = path;
return "OK";

The FileMaker server script could look like this:

// ----------------------------------------------------
// Script: psos_Report
// ----------------------------------------------------

Set Variable [ $x; Value: ACF_Run( "ExportACFlibList2Excel"; "" ) ]

If [ $x = "OK" ]
    Go to Layout [ “ServerReports” (ServerReports) ]
    New Record/Request
    Set Field [ ServerReports::ReportName; Get(ScriptParameter) ]
    Set Field [ ServerReports::FileName; ACFU_GetFilenameFromPath( $$FilePath ) ]

    // Insert the report into the container — the ACF way
    Set Field [ ServerReports::ReportContainer; ACF_Run( "MakeContainerFromPath"; $$FilePath ) ]

    Set Variable [ $Err; Value: "" ]
    If [ Get(LastError) <> 0 ]
        Set Variable [ $Err; Value: ¶ & Get(LastError) & ¶ & Get(LastErrorDetail) & ¶ & $$FilePath ]
    End If

    Commit Records/Requests [ With dialog: Off ]
    Exit Script [ "OK¶" & ServerReports::PrimaryKey & $Err ]
End If

Exit Script [ $x ]

1.5. 💻 Client-side script that runs the report

// ----------------------------------------------------
// Script: Run Report
// ----------------------------------------------------

Commit Records/Requests [ With dialog: Off ]
Perform Script on Server [ “psos_Report”; "List of ACF-functions in library" ]
Set Variable [ $$Result; Value: Get(ScriptResult) ]
Show Custom Dialog [ Title: "Result"; Message: $$Result; Default Button: “OK”, Commit: “Yes” ]

When run, you’ll see output similar to this — the second line is the primary key of the new ServerReports record, which can be used for linking or navigation:

dialog-run-report

And in the ServerReports table:

screen-server-reports


1.6. 📂 Retrieving and managing the report

From the client side, the report stored in the container can now be:

It’s highly recommended to configure the container field for external storage, so that reports are stored outside the main database file. This prevents your .fmp12 file from growing too large and keeps backups efficient.

1.7. Special considerations for PSOS and the OnFirstWindowOpen trigger

Every time a script runs with Perform Script on Server (PSOS), FileMaker starts a new server session. If your file has a startup script bound to File → Options → Triggers → OnFirstWindowOpen, that script will fire for each PSOS session as well. If the startup script does heavy initialization that isn’t needed on the server, add a guard so it only runs when appropriate.

One simple pattern is to expose a tiny ACF function that says whether your ACF packages are already loaded:

Package myACFpackage "My ACF functions collection";

function myACFpackage_AreWeLoaded ()
    return "Yes";
end

Then in your startup script:

If [ PatternCount ( Get ( ApplicationVersion ) ; "Server" ) > 0 ]
    // Running on Server (PSOS or Schedule) → do minimal init
    If [ ACF_Run ( "myACFpackage_AreWeLoaded" ) <> "Yes" ]
        Perform Script [ “InitACF” ]   // loads ACF packages once when needed
    End If
    Exit Script [ Result: 0 ]          // skip client-only initialization
End If

With this in place, the first PSOS (or scheduled) run will load your ACF packages; subsequent PSOS runs will see they’re already loaded and exit the startup script early. Client launches still run the rest of your startup script for UI-related initialization.