Parameters Deep Dive¶
Parameters define what data your command accepts from the LLM. They are declared as JarvisParameter objects in your command's parameters property, converted to JSON Schema for the LLM's tool interface, and validated by the execution pipeline before run() is called.
JarvisParameter Constructor¶
JarvisParameter(
name: str, # Parameter name (snake_case)
param_type: str, # Type string (see table below)
required: bool = False, # Whether the LLM must provide this
description: str | None = None, # Description shown to the LLM
default: str | None = None, # Default value
enum_values: list[str] | None = None, # Constrained values
refinable: bool = False, # Filled in by a second, focused model call (see below)
options_source: str | None = None, # Live option list for UIs (see below)
)
JarvisParameter is imported from jarvis_command_sdk.
options_source tells UIs such as the routine builder to fill a dropdown from live data instead of (or as well as) enum_values: "devices", "devices:<domain>" (e.g. "devices:light"), "rooms" or "news_categories". It is copied into the parameter's tool schema as options_source.
Supported Types¶
Primitive Types¶
| Type String | Aliases | JSON Schema Type | Python Type |
|---|---|---|---|
"string" |
"str" |
string |
str |
"integer" |
"int" |
integer |
int |
"float" |
number |
float |
|
"boolean" |
"bool" |
boolean |
bool |
"array" |
"list" |
array |
list |
"dict" |
object |
dict |
Datetime Types¶
| Type String | JSON Schema Type | Python Type |
|---|---|---|
"datetime" |
string |
datetime.datetime |
"date" |
string |
datetime.date |
"time" |
string |
datetime.time |
"timedelta" |
string |
datetime.timedelta |
Image Type¶
| Type String | Aliases | What run() receives |
|---|---|---|
"image" |
"array<image>" |
list[JarvisImage] |
An image parameter receives the photos the user attached in the mobile app's chat: always a list, in attach order, at most 4 images of at most 2 MiB each. execute() turns the wire value into JarvisImage objects before validate_call(); a bad image, or an empty list for a required parameter, comes back as a validation error.
Array Types¶
The date and time arrays accept three spellings; the other arrays only the angle-bracket one:
| Angle Bracket | Square Bracket | Shorthand | items in the tool schema |
|---|---|---|---|
"array<datetime>" |
"array[datetime]" |
"datetime[]" |
{"type": "string", "format": "date-time"} |
"array<date>" |
"array[date]" |
"date[]" |
{"type": "string", "format": "date"} |
"array<time>" |
"array[time]" |
"time[]" |
none |
"array<timedelta>" |
"array[timedelta]" |
"timedelta[]" |
none |
"array<string>" |
none | ||
"array<int>" |
none | ||
"array<float>" |
none | ||
"array<bool>" |
none |
The tool schema describes only datetime and date arrays' items; every other array reaches the model as a bare "type": "array", so say what the items are in the description.
Example -- weather command with date array:
JarvisParameter(
"resolved_datetimes",
"array<datetime>",
required=True,
description="Date keys: 'today', 'tomorrow', 'this_weekend', etc.",
)
Using an invalid type string raises a ValueError at class instantiation time.
Required vs. Optional¶
# Required -- LLM must extract this from the voice command
JarvisParameter("ticker", "string", required=True, description="Stock symbol")
# Optional with default -- LLM may omit, code uses default
JarvisParameter("count", "int", required=False, default="1", description="Number of dice")
# Optional without default -- LLM may omit, code checks for None
JarvisParameter("city", "string", required=False, description="City name. Omit for default.")
If a required parameter is missing, the execution pipeline raises a ValueError before run() is called.
Enum Values¶
Constrain a parameter to a fixed set of values:
JarvisParameter(
"operation",
"string",
required=True,
description="Arithmetic operation to perform",
enum_values=["add", "subtract", "multiply", "divide"],
)
The LLM sees these as an enum in the JSON Schema. During validation, if the value does not match any enum value, a ValidationResult error is generated with the list of valid values. The node sends that back to jarvisd as a failed tool result, and the model can call the tool again with a valid value.
Enum values are always strings, even for integer parameters. Compare as strings when validating.
Dynamic Enums¶
For values that come from runtime data (like device names), use validate_call() instead of static enum_values:
@property
def parameters(self) -> List[JarvisParameter]:
return [
JarvisParameter("action", "string", required=True, enum_values=[
"turn_on", "turn_off", "lock", "unlock",
]),
# entity_id has no static enum -- validated dynamically
JarvisParameter("entity_id", "string", required=False),
]
def validate_call(self, **kwargs) -> list[ValidationResult]:
results = super().validate_call(**kwargs)
entity_id = kwargs.get("entity_id")
if entity_id:
known = self._get_known_entities()
if entity_id not in known:
results.append(ValidationResult(
success=False,
param_name="entity_id",
command_name=self.command_name,
message=f"Device '{entity_id}' not found",
valid_values=known,
))
return results
The refinable Flag¶
A refinable parameter is left out of the first tool call and filled in by a second, focused model call. Use it for detail parameters that a small model tends to get wrong when it also has to pick the tool.
JarvisParameter(
"volume_level",
"int",
required=False,
description="Volume level 0-100",
refinable=True,
)
JarvisParameter(
"queue_option",
"string",
required=False,
enum_values=["play", "next", "add"],
description="Queue behavior",
refinable=True,
)
When refinable=True, the parameter schema includes a _refinable: true marker. On the text path, jarvisd removes refinable parameters from the schema the model sees when it picks the tool. Once the model has picked this tool, jarvisd makes a second, focused model call on the same utterance to fill in the refinable parameters, and adds them to the tool call before it goes to the node. control_device and get_device_status have their own resolvers and skip this step. It applies to the current utterance only; follow-up turns ("make it louder") are ordinary new turns.
Validation Pipeline¶
Parameters go through three layers of validation in the execute() pipeline:
1. Presence Validation (in execute())¶
Checks that all required=True parameters are present in kwargs. If any are missing, raises ValueError.
2. Type Validation (param.validate() -> _validate_type)¶
Checks that the value matches the declared type:
# These pass type validation:
JarvisParameter("count", "int") # value=5 -> OK
JarvisParameter("ratio", "float") # value=3 (int) -> OK (int accepted for float)
JarvisParameter("dates", "array<datetime>") # value=["2025-01-01"] -> OK (is a list)
# These fail:
JarvisParameter("count", "int") # value="five" -> FAIL
JarvisParameter("flag", "bool") # value="yes" -> FAIL
3. Enum Validation¶
If enum_values is set, checks that str(value) is in the list:
JarvisParameter("op", "string", enum_values=["add", "subtract"])
# value="add" -> OK
# value="multiply" -> FAIL: "Invalid value 'multiply' for 'op'. Must be one of: add, subtract"
4. Custom Validation Function¶
The IJarvisParameter abstract class supports a validation_function property for custom inline validation. When using JarvisParameter directly, use validate_call() on the command instead.
# On the abstract IJarvisParameter:
@property
def validation_function(self) -> Callable[[Any], bool] | None:
return lambda v: 0 <= v <= 100
@property
def validation_error_message(self) -> str:
return "Value must be between 0 and 100"
How Parameters Become Tool Schema¶
Your parameters are converted to JSON Schema for the LLM via to_openai_tool_schema():
# Given these parameters:
parameters = [
JarvisParameter("city", "string", required=False, description="City name"),
JarvisParameter("unit", "string", required=False, enum_values=["metric", "imperial"]),
JarvisParameter("dates", "array<datetime>", required=True, description="Target dates"),
]
# to_openai_tool_schema() returns:
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Weather conditions or forecast",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name"
},
"unit": {
"type": "string",
"enum": ["metric", "imperial"]
},
"dates": {
"type": "array",
"description": "Target dates",
"items": {"type": "string", "format": "date-time"}
}
},
"required": ["dates"]
}
},
"allow_direct_answer": false,
"is_risky": false,
"keywords": [...],
"examples": [...]
}
The top-level allow_direct_answer, is_risky, keywords, examples (and antipatterns, when declared) are Jarvis extensions, read by jarvisd when it builds the prompt. Where it passes the tools on as tool definitions, it removes allow_direct_answer, keywords, examples and the _refinable markers first.
Best Practices¶
- Use descriptive names --
citynotc,volume_levelnotvol - Write clear descriptions -- this is the primary way the LLM knows what to extract
- Make parameters optional when possible -- with sensible defaults in
run() - Use enums for constrained values -- reduces LLM errors significantly
- Keep parameter count low -- 1-5 parameters is ideal. More than 7 confuses smaller models.
- Use
critical_rulesfor mapping guidance -- when voice words do not match parameter values (e.g., "plus" -> "add") - Guard against LLM artifacts in
run()-- check for literal strings like"default","none","null"that the LLM might pass instead of omitting the parameter