How to develop scripts¶
This page explains how to write, load and manage q scripts: when each loading mechanism applies, how to package reusable code as a module, how to print and log, and how to prepare a script for distribution.
A script is a text file of q code. It lets you capture what you would otherwise enter interactively, define functions once and reuse them, and turn a q process into something specific — a real-time database, a historical database, a gateway, an analytics scheduler.
File extensions
.q denotes a script of q code. KDB-X also runs k code, which uses .k.
Any text editor works. There is a KX extension for Visual Studio Code, and Vim and Emacs both have q modes.
Load a script¶
At startup¶
Pass the filename as the first command-line argument. Any command-line options come after it, not before:
q testscript.q
q testscript.q -q # load the script in quiet mode
At run time¶
Use the \l system command:
q)\l test.q
Paths can be absolute or relative. With no path, q looks in the current directory and then in QHOME.
To build a path at run time, use the system form so the argument can be computed — here from an environment variable read with getenv:
q)system "l ",getenv[`TMPTEST],"/tmptest.q"
From another script¶
A script is just q code, so one script can load another. This is the simplest way to share common code:
\l utils.q
\l common.q
/ code goes here
It is also the mechanism with the fewest guarantees: \l re-runs the file every time it is called, so a script loaded twice defines its variables twice, and any initialization it performs happens again. For anything you intend to reuse, package it as a module instead.
Package code as a module¶
A module is the unit of reusable code in KDB-X. It solves two problems that plain scripts leave to you: loading the same code more than once, and keeping its internals out of everybody else's way.
A module is a script that defines a variable named export — a dictionary of the names it wants to publish:
/ mp/greet.q
-1"loading greet";
salutation:"Hello"
hello:{[n] salutation," ",n,"!"}
export:([hello])
The bracketed dictionary syntax ([hello]) derives the key from the variable name. Load it with use, which returns the export dictionary for you to name as you like:
q).Q.m.SP:enlist "mp" / search path; defaults to $QHOME/mod
q)g:use`greet
loading greet
q)g.hello "world"
"Hello world!"
Modules load once¶
use deduplicates. Ask for the same module again — from anywhere, including from inside another module — and you get the cached export without the body running a second time:
q)h:use`greet / no "loading greet" this time
q)h.hello "again"
"Hello again!"
That removes the need for the guard blocks that scripts require. Where a plain script has to test a flag to avoid re-initializing itself, a module cannot run its body twice.
Modules keep their names private¶
Everything a module defines other than its export stays in the module's own namespace. salutation above is invisible outside it, and the only names added to the root namespace are the ones the caller chose:
q)salutation
'salutation
[0] salutation
^
q)system"v"
`s#`g`h
So two modules can both define hello without colliding, and neither can overwrite anything of yours.
Namespaces, and why they matter less
Before modules, the way to avoid name clashes was a custom namespace. That still works, and you meet it in existing code.
Refer to a namespace to create it:
q).mycode.myvar:22
q).mycode.myfunc:{.mycode.myvar+x}
q).mycode.myfunc[22]
44
Or set the current namespace with \d, returning to root at the end of the script:
\d .mycode
myvar:22
myfunc:{myvar+x}
\d .
Namespaces are dictionaries, so entering the name shows the contents:
q).mycode
| ::
myvar | 22
myfunc| {.mycode.myvar+x}
Two things to know: namespaces can nest, and single-character namespaces are reserved for KX, so do not define your own.
For new code, prefer a module. A namespace is a convention that relies on everyone respecting it — a module is enforced, and it handles load-once as well.
Write a script¶
Execution order¶
q executes a script as it loads it, top to bottom. This basic.q prints markers around its definitions:
-1"first line";
a:33
myfunc:{x+x}
-1"last line";
Loading it runs both prints and leaves the variable and function defined:
q)\l basic.q
first line
last line
q)a
33
q)myfunc[22]
44
When a script has a bad line¶
q stops at the first error and does not run the rest of the file. Given bad.q:
-1"about to run some bad code";
bad_code_here
-1"running after bad code";
the third line never runs. q reports where it failed and suspends; \ returns to the normal prompt:
q)\l bad.q
about to run some bad code
'bad_code_here
[3] /private/tmp/bad.q:2: bad_code_here
^
q))\
q)
Handle command-line parameters¶
A script loaded at startup usually needs configuration — which database to open, which port to listen on, how much to log. q exposes the command line in two forms, and .Q provides the parsing.
.z.x and .z.X¶
.z.f holds the script name. .z.x holds the arguments that follow it, with the script name and q's own single-letter options removed:
$ q test.q -P 0 -abc 123
q).z.f
`test.q
q).z.x
"-abc"
"123"
-P 0 is one of q's options, so q consumed it and your script never sees it. .z.X gives the unfiltered line instead, including the executable and q's own options:
$ q somefile.q -customarg 42 -p localhost:17200
q).z.X
,"q"
"somefile.q"
"-customarg"
"42"
"-p"
"localhost:17200"
Do not name a parameter after one of q's options
q claims the single-letter options it recognizes before the script runs, so a parameter called -p, -s, or -q never reaches .z.x — it silently changes how q itself starts. Give your own parameters multi-character names.
Parse with .Q.opt¶
.Q.opt turns .z.x into a dictionary keyed by parameter name, with the leading - stripped:
$ q script.q -param1 val1 -param2 val2
q).Q.opt .z.x
param1| "val1"
param2| "val2"
A parameter's value is whatever follows it up to the next -. Every value is a list of strings, whatever its length: none gives the empty list, one gives a one-item list, several give one item each.
$ q script.q -flag -one val1 -many aaa bbb
q).Q.opt .z.x
flag| ()
one | ,"val1"
many| ("aaa";"bbb")
A single value is still a list
The console renders a one-item list of strings in a way that is easy to misread as a plain string, so check rather than assume:
q)o:.Q.opt .z.x
q)(type o`one; count o`one) / general list, one item
0h 1
q)10h = type o`one / not a string
0b
So unwrap it — first o`one, not o`one — or use .Q.def below, which does that for you.
Add defaults and types with .Q.def¶
.Q.def takes a dictionary of defaults and the output of .Q.opt, fills in anything the caller omitted, and casts each supplied value to the type of its default. That is what removes the string handling:
/ start.q
cfg:.Q.def[([db:`:/tmp/hdb; port:5000i; verbose:0b])] .Q.opt .z.x
-1"db : ",.Q.s1 cfg`db;
-1"port : ",.Q.s1 cfg`port;
-1"verbose : ",.Q.s1 cfg`verbose;
With nothing supplied, every value is the default:
$ q start.q -q
db : `:/tmp/hdb
port : 5000i
verbose : 0b
Supplied values arrive already converted — port is an int and verbose a boolean, not strings:
$ q start.q -q -db /data/hdb -port 5010 -verbose 1
db : `/data/hdb
port : 5010i
verbose : 1b
Defaults must be atoms, since q infers the type from them. A value that cannot be converted becomes a null rather than an error, so validate anything that matters.
A file-symbol default does not produce a file symbol
Look closely at db above: the default is `:/tmp/hdb but the override came back as `/data/hdb — a plain symbol. .Q.def casts to the default's type, which is symbol, and the leading colon is not part of the type.
Normalize the path yourself. hsym is idempotent, so it is safe to apply either way:
cfg[`db]:hsym cfg`db
$ q start.q -q -db /data/hdb
db (hsym'd): `:/data/hdb
Positional arguments with .Q.x¶
Arguments without a leading - are not parameters. .Q.x collects them, and calling .Q.opt sets it as a side effect — pass .z.X rather than .z.x to include the executable and the positional arguments:
$ q taq.k path/to/source path/to/destn
q)cla:.Q.opt .z.X / also populates .Q.x
q).Q.x
"/Users/me/q/m64/q"
"path/to/source"
"path/to/destn"
Print output¶
Write to stdout, stderr, or a file using the corresponding file handle. Negative handles append a newline; positive ones do not:
-1 "this prints with a return character appended to stdout";
-2 "this prints with a return character appended to stderr";
1 "this prints without a return character appended to stdout";
q)\l output.q
this prints with a return character appended to stdout
this prints with a return character appended to stderr
this prints without a return character appended to stdoutq)
Because the argument is a string, compose it as one. .Q.s1 renders any value as its console representation, which is what you want for a timestamp from .z.P:
-1 "Current time ",.Q.s1 .z.P;
q)\l output.q
Current time 2025.05.28D11:29:57.452093632
For logging, use the Logging module
-1 and -2 are right for a progress message or a one-off diagnostic. They are not a logging framework: there are no severity levels, no structure, no way to route or filter messages, and every caller invents its own format.
For anything a process emits in production, use the Logging module. It attaches timestamps, severity levels, and component names, and can emit structured JSON. It also routes messages to file descriptors or external services with rules for filtering and suppression, which is what makes logs usable across a distributed deployment.
Multiline expressions¶
A script can split a long expression across lines. Continuation lines must be contiguous — no blank lines — and indented by at least one space:
jt:([forename: "Jacques";
family: "Tati";
dob: 1907.10.09;
dod: 1982.11.05;
spouse: "Micheline Winter";
children: 3;
pic: "https://en.wikipedia.org/wiki/Jacques_Tati#/media/File:Jacques_Tati.jpg" ])
portrait:{
n:" "sv x`forename`family; / name
i:.h.htac[`img;`alt`href!(n;x`pic);""]; / img
a:"age ",string .[-;x`dod`dob]div 365; / age
c:", "sv(n;"d. ",4#string x`dod;a); / caption
i,"<br>",.h.htac[`p;([style:"font-style:italic"]);c] }
Both definitions use the bracketed dictionary syntax, which names each entry inline and keeps a long dictionary readable down the page.
Reserved words cannot be dictionary keys
The bracketed syntax derives each key from the name before the colon, so a name that is already a q keyword is an assignment error:
q)([first: "Jacques"])
'assign
That is why the entry above is forename. Where you need a key that collides with a keyword, build the dictionary from explicit symbols instead — (`first`family)!("Jacques";"Tati").
Comments¶
A / starts a comment, either on its own line or after an expression:
a:22 / this is a comment
Scripts additionally support comment blocks.
Multiline comments¶
Open with a line containing only / and close with a line containing only \:
/
This is a comment block.
q ignores everything in it.
And I mean everything.
2+2
\
Trailing comments¶
A lone \ that does not close a block starts a trailing comment: q ignores everything after it, and there is no way to end it.
That makes it a useful way to keep a script's entry point out of the load. Here the script defines its functions when loaded, leaving the session available to explore. The \ parks the two lines that run and exit:
foo:42
bar:"quick brown fox"
main:{x,y}
\
main[foo;bar]
exit 0
Find information about a script¶
The name of the initial script¶
.z.f holds the filename passed to q on the command line. A myscript.q containing -1"Executing script ",string .z.f; prints:
$ q myscript.q -q
Executing script myscript.q
Find where a function is defined¶
value on a lambda returns its internals, including the script it came from:
q)value f
0x6261410003
`x`y
`symbol$|()
,`
5 3 4 2 2
"..f"
"/private/tmp/test.q"
1
"{x+y}"
The script path is the third element from the end. The number of elements varies between functions, so take it relative to the length rather than at a fixed index:
q)first -3# value f
"/private/tmp/test.q"
A function defined at a prompt rather than in a file has an empty string there. The same idiom works inside the debugger on .z.s, to find out where the function you are suspended in came from — see the function you are inside.
Run a script as a shebang¶
To make a script a standalone executable, give it a shebang line:
$ more ./test.q
#!/usr/bin/env q
2+3
\\
$ chmod +x ./test.q
$ ./test.q
KDB-X 5.0.20251113 2025.11.13 Copyright (C) 1993-2025 Kx Systems
...
5
Obfuscate code for distribution¶
To distribute code without shipping readable source, \_ writes an obfuscated copy of a script. Given hide.q:
hiddenFunc:{val+x+y};
val:55;
Loaded normally, the source is there for anyone to read, in the file and at run time:
q)\l hide.q
q)hiddenFunc
{val+x+y}
\_ produces a new file with a trailing underscore:
q)\_ hide.q
`hide.q_
Its contents are no longer legible:
$ cat hide.q_
016 262 207 313 } 226 331 306 020 335 016 303 374 , , \r ...
Load that instead and the function still works, but its definition reads as locked and value yields no source and no script path:
q)\l hide.q_
q)hiddenFunc
locked
q)hiddenFunc[2;5]
62
q)value hiddenFunc
`byte$()
`x`y
`symbol$()
``val`
`long$()
"..hiddenFunc"
""
-1
"locked"
This is obfuscation, not encryption
There is no key and nothing is decrypted at load time — \_ removes the source text and scrambles the bytecode so it cannot be read back. It raises the effort needed to inspect your logic; it is not a security control, so do not rely on it to protect a secret.
Its scope is narrow, too. Only the bodies of functions become unreadable:
-
Data is untouched.
valis still there to read and to change:q)val 55 q)val:0 q)hiddenFunc[2;5] 7 -
You can still redefine a locked function, so the behavior is not protected either:
q)hiddenFunc:{y-x} q)hiddenFunc[2;5] 3
Keep the original
\_ does not keep a readable copy. Delete hide.q and the source is gone — treat the obfuscated file as a build artifact and keep the original in version control.
Next steps¶
- Package reusable code properly: module framework and its quickstart.
- Emit production diagnostics with the Logging module.
- Diagnose a script that fails with debug q code.
- Write the functions the script holds: how to work with functions.
- Read the references: the
\lload command, the\ddirectory command, the\_hide-code command, command-line options, namespaces, and for parameters.z.x,.Q.opt, and.Q.def.