IR
irwinrodriguez.dev
Back to docs

API Reference

Complete reference for FoxCore.Runtime 3.4.2: the six COM types, their members, and what each returns when something goes wrong.

Valid for FoxCore.Runtime 3.4.2 (2026-09-15). The contract is frozen at six types; members always grow at the end, so nothing on this page changes name or signature in a later version.

Starting the engine

The minimum that works: create, activate the license, attach to the thread, launch a task, pump while it runs, and stop.

Starting the engine: from CREATEOBJECT() to Shutdown()
LOCAL loRt, lcId

loRt = CREATEOBJECT("FoxCore.Runtime")
? loRt.Activate(lcJwt)                    && .T. -- your license, as a string
loRt.Attach(_VFP)                         && attaches the runtime TO THIS thread
loRt.MaxWorkers = 8

lcId = loRt.RunScript("RETURN 6 * 42", "la primera")   && the script runs in ANOTHER VFP process

DO WHILE !loRt.IsCompleted(lcId)
    loRt.Pump()                           && Pump() serves the workers; without it they just wait
    DOEVENTS
ENDDO

? loRt.GetState(lcId)                     && Completed
? loRt.GetResult(lcId)                    && 252
? loRt.GetError(lcId)                     && ""

loRt.Shutdown()                           && ALWAYS: it is what kills the workers

The JWT is passed in code: there is no file to copy. How you get it and what happens when it is missing is on the Licensing page.

The six types

Three classes and their three interfaces. There is no seventh type, and there will not be: twice something looked like it needed one - TryCall's result, then GetStats - and both times the answer was a JSON string.

ClassInterfaceProgIdWhat it is
FoxRuntime IFoxRuntime FoxCore.Runtime The object the host creates. It is the engine: license, options, tasks, pumping, events and handlers.
FoxTaskHandle IFoxTask — The handle NewTask() returns. Sugar: it holds no state, every property asks the registry.
WorkerBridge IFoxWorker — The runtime variable INSIDE the script. It is all the worker sees of the main process.

FoxCore.Runtime

Lifecycle

MemberReturnsNotes
Activate(cLicenseKey)LValidates the JWT. It NEVER throws on an invalid license: it returns .F. and what gets blocked are the tasks, not the lifecycle.
Attach(oHost, nVersion, cRuntimeMode)—Attaches the runtime TO THE THREAD it is called from. Usually loRt.Attach(_VFP). The last two are optional.
Shutdown()—Idempotent. IT IS WHAT KILLS THE WORKERS. Always call it.
Ping()C"FoxCore.Runtime 3.4.2". Works in any state, even after Shutdown().
VersionCRead-only.
StateCCreated | Activated | Attached | ShuttingDown | ShutDown. A STRING, not an enum: no numeric value crosses COM.
IsRunningL.T. between Attach() and Shutdown().
IsLicensedL.T. if the JWT is valid. Since 3.4.0 there is NO exception by mode: it is required outside the IDE too.

Options

All read/write. Set them after creating the object and before launching tasks.

PropertyDefaultWhat it controls
MaxWorkersmin(nucleos, 8)Simultaneous worker processes. Range 1-64. Rule of thumb: physical cores.
DefaultTimeoutSeconds30Deadline per task. Range 1-86,400. On expiry the task ends TimedOut and the worker dies.
WorkerProgId"VisualFoxpro.Application.9"Which process acts as worker: VFP 9, VFP Advanced or your own registered EXE. The FORMAT is validated, not the existence: to know whether it works, ProbeWorker().
KeepCompletedTasks200How many FINISHED tasks the registry keeps before purging the oldest. It matters in large batches.
MaxTasksPerWorker1How many tasks a worker serves before being recycled. 1 = one process per task, full isolation. 0 = no limit.
WorkerInitScript""Script that runs ONCE PER WORKER, before its first task. This is where the environment goes.
WorkerResetScript""Script that runs BETWEEN TASKS of a reused worker. This is where you clean up.
WorkerInitFailurePolicy"abort"What happens if the init script errors: "abort" takes it down, "tolerate" lets its own ON ERROR take over and carry on.
PumpMaxItems8How much each Pump() serves.
PumpMaxMilliseconds20Time budget of each Pump().
RequestTimeoutSeconds15How long a worker blocked in Call() waits before getting a Timeout. It is what stops a host that does not pump from hanging a worker forever.
SecurityMode"Safe""Safe" is the only real mode.
AuditLevel"basic"off | basic | full.

Tasks

MemberReturnsNotes
RunScript(cScript, cTaskName)CCreates, queues and returns the task id. THE NORMAL WAY.
NewTask(cScript, cTaskName)FoxTaskHandleCreates WITHOUT queueing, so you can configure it first (for instance with SetTaskTimeout).
Start(cTaskId)LQueues a created task. .F. if it does not exist or was already started.
GetState(cTaskId)CCreated | Queued | Running | Completed | Failed | Cancelled | TimedOut, and NotFound for an unknown or already purged id.
GetResult(cTaskId)valorThe script's RETURN. .NULL. until it has finished successfully; an empty string for an unknown id.
GetError(cTaskId)CAn empty string if it did not fail; "Task not found." for an unknown id.
IsCompleted(cTaskId)L.T. in any terminal state AND ALSO for an id that does not exist - on purpose: otherwise the canonical loop would spin forever once the purge took the task away.
GetProgress(cTaskId)N0-100. It works even if nobody pumps.
GetProgressMessage(cTaskId)CThe last message, or an empty string.
PollCompleted()CThe ids finished SINCE THE LAST CALL, separated by ";". One COM call per round instead of N. READING IT EMPTIES IT.
SetTaskTimeout(cTaskId, nSegundos)LDeadline for ONE task. Only before it starts: NewTask + this + Start. .F. if it already started or does not exist.
Forget(cTaskId)LForgets a FINISHED task and frees its result. .F. if it does not exist or is still alive.

Cancellation

MemberReturnsNotes
Cancel(cTaskId)LCOOPERATIVE: raises the flag the script reads with runtime.IsCancelled() and instantly unblocks any Call in flight. .F. if there is nothing to stop.
CancelAll()NHow many it canceled.
Kill(cTaskId)LHARD: kills the process by PID. For a script that never checks the flag it is the only way. It ends Cancelled with "Killed by host."

Pumping

MemberReturnsNotes
Pump(nMaxItems, nMaxMs)NServes the synchronous requests first - behind each one there is a BLOCKED worker - and the notifications after. Both parameters at 0 or absent = use the options.
Three rules of Pump() you need to know: it can only be called from the Attach thread (from another one it raises a controlled error); it is not reentrant - called while already pumping it returns 0 doing nothing, without throwing; and after Shutdown() it returns 0, with no error.

Events

MemberReturnsNotes
SetEventSink(oSink)LThe runtime notifies FROM INSIDE Pump(), on your thread. It returns .F. WITHOUT THROWING if the object lacks the required methods.
ClearEventSink()—Release it in the form's Destroy: while the runtime holds it, the form stays alive.

The sink must have OnProgress(tcTaskId, tnPercent, tcMessage) and OnLog(tcTaskId, tcLevel, tcMessage), both required, and may have OnTaskCompleted(tcTaskId, tcState) and OnTaskFailed(tcTaskId, tcError), detected if present. The sink is additional: GetProgress() and GetProgressMessage() work just the same without it.

Handlers - what a worker can ask the host for

MemberReturnsNotes
RegisterHandler(cName, oTarget, cMethodName)LcMethodName defaults to "Invoke". It returns .F. WITHOUT THROWING if the name is empty, has spaces, exceeds 64 characters, is already registered, is one of the built-ins, OR IF THE OBJECT DOES NOT EXPOSE THAT METHOD - that last check makes the error surface in the form Init and not six hours later inside a worker.
UnregisterHandler(cName)L.F. if it was not registered.
GetHandlerNames()CThe names separated by ";", alphabetical. It is the COMPLETE surface of what a worker can ask for. Three built-ins are always available: echo, upper and add.

Shared blackboard

It solves "the worker needs the host's configuration" with no round trip and no pump.

MemberReturnsNotes
Publish(cName, uValue)—Publishes a value the workers read with runtime.GetValue() with NO queue and NO pump cost.
GetValue(cName)valor.NULL. if it does not exist.

Other

MemberReturnsNotes
GetStats()CThe engine snapshot as a flat JSON STRING. It returns a string and not an object for two reasons: a COM object costs one round trip PER PROPERTY, and adding a new type would break the six-type contract.
ProbeWorker(cProgId)CDoes that ProgId work as a worker? The answer comes from RUNNING it. With no argument it probes the current WorkerProgId.
GetAuditPath()CWHERE the journal WOULD go. It writes nothing.
SetCorrelation(cId, cName, cValue)LA CLOSED vocabulary of seven names. .F. without writing for anything else.
GetCorrelation(cId, cName)CAn empty string if not set.

What GetStats() returns

A flat JSON string, parsed with jsonfox in a single call. There are two families of counters and confusing them is expensive:

  • createdEver, finishedEver, completedEver, failedEver, cancelledEver, timedOutEver - they count what has HAPPENED since the runtime started and they NEVER go down. These are the ones for painting progress.
  • total, created, completed, failed, cancelled, timedOut - they are the SNAPSHOT of the registry right now, and they do go down: Forget() and the purge subtract from them. These tell you what is alive.
GetStats() with a batch of 30 tasks under way
{
  "state": "Attached",
  "workerProgId": "VisualFoxpro.Application.9",
  "maxWorkers": 8,
  "queued": 22,
  "running": 8,
  "peakRunning": 8,
  "liveWorkers": 8,
  "createdEver": 30,
  "finishedEver": 0,
  "completedEver": 0,
  "failedEver": 0,
  "cancelledEver": 0,
  "timedOutEver": 0,
  "total": 30,
  "created": 0,
  "completed": 0,
  "failed": 0,
  "cancelled": 0,
  "timedOut": 0,
  "purged": 0,
  "keepCompletedTasks": 200,
  "completedPending": 0,
  "completedDropped": 0,
  "pendingRequests": 0,
  "expiredRequests": 0,
  "pendingNotifications": 3,
  "droppedTelemetry": 0,
  "handlers": 2,
  "requestsHandled": 47,
  "handlerErrors": 0
}
The snapshot is not atomic: each counter is read under its own lock. It is good for painting a progress bar, not for balancing a sum. The truth about one task is always GetState(id).

FoxTaskHandle

Returned by NewTask(). It holds no state: every property delegates to the engine's registry. It is sugar - everything it does can be done with the taskId and the runtime methods.

MemberTypeNotes
IdCThe taskId. THE CANONICAL TRUTH is this string: it crosses COM with no lifetime cost and it is what you paste into a ticket.
NameCThe name given when it was created.
StateCThe same as GetState(Id).
ResultvalorThe same as GetResult(Id).
ErrorCThe same as GetError(Id).
IsCompletedLThe same as IsCompleted(Id).
OperationIdCIt exists and is ALWAYS an empty string: operations are not implemented.
Start()LQueues the task. The same as Start(Id).

WorkerBridge - the runtime variable inside the script

It is all the worker sees of the main process, and its surface is scoped to that task. The "Blocks?" column is the one that matters: only Call and TryCallJson wait for the host to pump, because they are the only ones that run VFP code in the main process.

MemberBlocks?Notes
TaskId / TaskNamenoThe id and name of THIS task.
ElapsedSecondsnoSeconds since the task started.
Ping()no"pong".
Progress(nPercent, cMessage)noFire-and-forget. It needs no pump and does not fail. The percentage is clamped to 0-100.
Log(cLevel, cMessage)no"info" | "warn" | "error". Fire-and-forget.
IsCancelled()noA LOCAL read, it does not touch the host. Meant to be called on every turn of a loop.
GetValue(cName) / SetValue(cName, uValue)noShared blackboard, local, no pump.
Call(cName, arg1..arg8)SICalls a host handler. Synchronous: the worker waits for the host to pump. It THROWS an OLE error if the handler fails.
TryCallJson(cName, arg1..arg8)SILike Call but it NEVER THROWS. It returns JSON: {"ok":true,"value":"42","error":"","errorCode":""}. The value always travels AS A STRING.

Patterns

The two loops you need to know:

  1. Normal batch: launch with RunScript(), keep the ids, and spin a DO WHILE calling Pump() and DOEVENTS until IsCompleted() says yes for all of them.
  2. Large batch (hundreds or thousands): asking task by task is N COM calls per round. Raise KeepCompletedTasks, call PollCompleted() once per round, read the result of the ids it hands you, and call Forget() on each. Progress comes from finishedEver, not from completed: with Forget() every round the snapshot stays at zero, and that is correct.

Both of them, with their full code and the traps of each, are on the Patterns page.

What no longer exists

If you arrive from a search engine or from code written for the classic engine (WinFx.FoxCore), this is what changed. The classic engine still exists and is still supported; it is not the one described on this page.

Classic engineIn FoxCore.Runtime 3.4.2
CREATEOBJECT("WinFx.FoxCore") / _screen.FoxCoreCREATEOBJECT("FoxCore.Runtime") - a different COM object, with a different ProgId and GUID. Both can be registered at the same time.
Run(cProc, oParams)RunScript(cScript, cName). You no longer pass the NAME of a procedure that must exist in the worker: you pass the SCRIPT, as text, and the worker runs it with EXECSCRIPT().
RunMultiple(@laTareas)No equivalent, on purpose: you call RunScript() in a FOR and keep the ids. A method taking an array by reference does not cross COM reliably, and what it returned was a container object: one more type, and the contract is six.
WaitAll() / WaitAny()No equivalent, and this is the deep difference: the loop is written by the HOST, with Pump() and DOEVENTS. A blocking WaitFor inside VFP is exactly the frozen UI this product exists to avoid.
GetTasksSummary() / GetSystemStats()GetStats() for the numbers (queued, running, liveWorkers) and PollCompleted() to know who just finished. An array of Task objects over COM costs one round trip per property per task.
CancelTask(loTask)Cancel(cTaskId). Identified by id, not by object.
Activate(cClave)Activate(cJwt), same name but a different key: since 3.4.0 there is no file, the license is a JWT passed in code.
foxcore.licNo equivalent: since 3.4.0 there is no license file. See the Licensing page.
PROCEDURE MiTarea(loParams, loTask)Inside the script there are no parameters: there is a variable called runtime (WorkerBridge). loTask.Progress = 50 becomes runtime.Progress(50, "...") and loTask.Result = x becomes RETURN x.

Next: Licensing ->